#!/usr/bin/env bash
# =====================================================================
#  DIAGNOSTICO DE EQUIPO CLIENTE - AUTHENTIC (2Tec Sistemas)
#  Version macOS / Linux
#
#  Uso:   bash Diagnostico_Authentic_Unix.sh [--url https://api.ejemplo.cl]
#
#  - No requiere sudo. Compatible con bash 3.2 (bash por defecto de macOS).
#  - Ejecuta en el equipo del cliente: hardware, dispositivos, estado del
#    sistema, red local, DNS, TCP, latencia/jitter, MTU, traceroute,
#    HTTP/HTTPS, proxy/firewall y un resumen ejecutivo.
#  - Las pruebas de endpoints, latencia de aplicacion y velocidad se
#    ejecutan desde el panel web de Authentic (no se repiten aqui).
#  - PRIVACIDAD: el informe se guarda solo en este equipo. El script no
#    envia el informe ni ningun dato a ningun servidor.
#  - Si una herramienta no existe o falla, se informa "No disponible" y se
#    continua: el script nunca aborta (no usa set -e).
# =====================================================================

SCRIPT_VERSION="1.0.0"

# ---------------------------------------------------------------------
# CONFIGURACION (el servidor reemplaza estos valores al descargar)
# ---------------------------------------------------------------------
CFG_API_URL="http://127.0.0.1:30"
CFG_HOST="127.0.0.1"
CFG_PORT="30"
CFG_AMBIENTE="Desarrollo"

DEFAULT_API_URL="https://api.authentic.cl"
DEFAULT_HOST="api.authentic.cl"
DEFAULT_PORT="443"

# Salida de comandos en formato estable (punto decimal, mensajes en ingles)
export LC_ALL=C
export LANG=C

NL='
'
UA="Authentic-Diagnostics-Unix/${SCRIPT_VERSION}"

# ---------------------------------------------------------------------
# Argumentos
# ---------------------------------------------------------------------
uso() {
    echo "Uso: bash $0 [--url <URL base de la API>]"
    echo "Ejemplo: bash $0 --url https://api.authentic.cl"
}

ARG_URL=""
while [ $# -gt 0 ]; do
    case "$1" in
        --url) shift; ARG_URL="$1" ;;
        --url=*) ARG_URL="${1#--url=}" ;;
        -h|--help) uso; exit 0 ;;
        *) echo "Argumento no reconocido: $1 (se ignora)" ;;
    esac
    [ $# -gt 0 ] && shift
done

# Devuelve 0 si el valor es un marcador sin reemplazar (o vacio)
es_marcador() {
    case "$1" in
        __AUTHENTIC_*|"") return 0 ;;
    esac
    return 1
}

# Separa una URL en esquema, host y puerto: imprime "esquema host puerto"
parsear_url() {
    local u="$1" esquema resto hp h p
    case "$u" in
        https://*) esquema="https" ;;
        http://*)  esquema="http" ;;
        *)         esquema="https"; u="https://$u" ;;
    esac
    resto="${u#*://}"
    hp="${resto%%/*}"
    hp="${hp##*@}"
    case "$hp" in
        \[*) h="${hp#\[}"; h="${h%%\]*}"; p="${hp##*\]}"; p="${p#:}" ;;
        *:*) h="${hp%%:*}"; p="${hp##*:}" ;;
        *)   h="$hp"; p="" ;;
    esac
    if [ -z "$p" ]; then
        if [ "$esquema" = "https" ]; then p=443; else p=80; fi
    fi
    echo "$esquema $h $p"
}

# Resolver configuracion efectiva
if [ -n "$ARG_URL" ]; then
    API_URL="$ARG_URL"
    case "$API_URL" in http://*|https://*) ;; *) API_URL="https://$API_URL" ;; esac
    read -r API_SCHEME API_HOST API_PORT <<EOF
$(parsear_url "$API_URL")
EOF
else
    if es_marcador "$CFG_API_URL"; then API_URL="$DEFAULT_API_URL"; else API_URL="$CFG_API_URL"; fi
    read -r API_SCHEME U_HOST U_PORT <<EOF
$(parsear_url "$API_URL")
EOF
    if es_marcador "$CFG_HOST"; then
        if es_marcador "$CFG_API_URL"; then API_HOST="$DEFAULT_HOST"; else API_HOST="$U_HOST"; fi
    else
        API_HOST="$CFG_HOST"
    fi
    if es_marcador "$CFG_PORT"; then
        if es_marcador "$CFG_API_URL"; then API_PORT="$DEFAULT_PORT"; else API_PORT="$U_PORT"; fi
    else
        API_PORT="$CFG_PORT"
    fi
fi
API_URL="${API_URL%/}"
case "$API_PORT" in ''|*[!0-9]*) API_PORT=443 ;; esac
if es_marcador "$CFG_AMBIENTE"; then
    if [ "$API_URL" = "$DEFAULT_API_URL" ]; then AMBIENTE="Producción"; else AMBIENTE="No informado"; fi
else
    AMBIENTE="$CFG_AMBIENTE"
fi
ES_DESARROLLO=0
case "$AMBIENTE" in Desarrollo|desarrollo|DESARROLLO) ES_DESARROLLO=1 ;; esac

# ---------------------------------------------------------------------
# Utilidades generales
# ---------------------------------------------------------------------
tiene() { command -v "$1" >/dev/null 2>&1; }

# Plataforma
UNAME_S="$(uname -s 2>/dev/null)"
case "$UNAME_S" in
    Darwin) OS="mac";   PLATAFORMA="macOS" ;;
    Linux)  OS="linux"; PLATAFORMA="Linux" ;;
    *)      OS="otro";  PLATAFORMA="Otro (${UNAME_S:-desconocido})" ;;
esac

# Comando timeout (GNU / Homebrew coreutils). Si no existe se usa un vigilante propio.
TIMEOUT_BIN=""
if tiene timeout && timeout 2 true >/dev/null 2>&1; then
    TIMEOUT_BIN="timeout"
elif tiene gtimeout && gtimeout 2 true >/dev/null 2>&1; then
    TIMEOUT_BIN="gtimeout"
fi

# run_to <segundos> <comando...>: ejecuta con tiempo maximo acotado
run_to() {
    local t="$1"; shift
    if [ -n "$TIMEOUT_BIN" ]; then
        "$TIMEOUT_BIN" "$t" "$@"
        return $?
    fi
    "$@" &
    local pid=$!
    ( sleep "$t"; kill -TERM "$pid" 2>/dev/null; sleep 2; kill -KILL "$pid" 2>/dev/null ) >/dev/null 2>&1 &
    local vig=$!
    wait "$pid" 2>/dev/null
    local rc=$?
    kill "$vig" 2>/dev/null
    wait "$vig" 2>/dev/null
    return $rc
}

# Reloj en milisegundos (date %N en GNU; perl/python en macOS)
RELOJ="s"
detectar_reloj() {
    local x
    x="$(date +%s%N 2>/dev/null)"
    case "$x" in
        *[!0-9]*|"") ;;
        *) if [ "${#x}" -ge 16 ]; then RELOJ="ns"; return; fi ;;
    esac
    if tiene perl && perl -MTime::HiRes=time -e 1 >/dev/null 2>&1; then RELOJ="perl"; return; fi
    if tiene python3 && python3 -c 'import time' >/dev/null 2>&1; then RELOJ="py"; return; fi
    RELOJ="s"
}
now_ms() {
    local x
    case "$RELOJ" in
        ns)   x="$(date +%s%N)"; echo $(( x / 1000000 )) ;;
        perl) perl -MTime::HiRes=time -e 'printf("%d\n", time()*1000)' ;;
        py)   python3 -c 'import time; print(int(time.time()*1000))' ;;
        *)    echo $(( $(date +%s) * 1000 )) ;;
    esac
}
detectar_reloj

# Comparaciones decimales con awk: fgt a b  => a > b ; flt a b => a < b
fgt() { awk -v a="$1" -v b="$2" 'BEGIN{ exit !((a+0) > (b+0)) }'; }
flt() { awk -v a="$1" -v b="$2" 'BEGIN{ exit !((a+0) < (b+0)) }'; }
redondear() { awk -v a="$1" -v d="${2:-2}" 'BEGIN{ printf("%." d "f", a+0) }'; }

# Estadisticas de una lista (un numero por linea en stdin):
# imprime "n prom min max p50 p95 p99 desv"
calc_stats() {
    grep -E '^-?[0-9]+(\.[0-9]+)?$' | sort -n | awk '
        function pc(p,   x, i) { x = (p / 100) * n; i = int(x); if (i < x) i++; if (i < 1) i = 1; if (i > n) i = n; return v[i] }
        { n++; v[n] = $1 + 0; s += $1 }
        END {
            if (n == 0) { print "0 0 0 0 0 0 0 0"; exit }
            m = s / n
            for (i = 1; i <= n; i++) { d = v[i] - m; q += d * d }
            printf("%d %.2f %.2f %.2f %.2f %.2f %.2f %.2f\n", n, m, v[1], v[n], pc(50), pc(95), pc(99), sqrt(q / n))
        }'
}

# Valor de una linea "Clave: valor" (system_profiler, diskutil, lscpu, etc.)
kv() {
    printf '%s\n' "$1" | awk -v k="$2" '{
        l = $0; sub(/^[ \t]+/, "", l)
        if (index(l, k ":") == 1) { v = substr(l, length(k) + 2); sub(/^[ \t]+/, "", v); sub(/[ \t]+$/, "", v); print v; exit }
    }'
}

leer() { [ -r "$1" ] && head -n 1 "$1" 2>/dev/null | tr -d '\r'; }

minus() { printf '%s' "$1" | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'; }

es_ipv4() { printf '%s' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; }

es_loopback() {
    case "$1" in
        localhost|localhost.*|127.*|::1|0.0.0.0) return 0 ;;
    esac
    return 1
}

ND="No disponible"

# ---------------------------------------------------------------------
# Colores (solo si la salida es una terminal)
# ---------------------------------------------------------------------
if [ -t 1 ]; then
    C_RST='\033[0m'; C_VERDE='\033[32m'; C_AMAR='\033[33m'; C_ROJO='\033[31m'
    C_GRIS='\033[90m'; C_CIAN='\033[36m'; C_MAG='\033[35m'
else
    C_RST=''; C_VERDE=''; C_AMAR=''; C_ROJO=''; C_GRIS=''; C_CIAN=''; C_MAG=''
fi

# ---------------------------------------------------------------------
# Archivo de informe
# ---------------------------------------------------------------------
EQUIPO="$(hostname 2>/dev/null || uname -n 2>/dev/null)"
EQUIPO="$(printf '%s' "$EQUIPO" | tr -d '\r\n')"
EQUIPO_CORTO="${EQUIPO%%.*}"
[ -z "$EQUIPO_CORTO" ] && EQUIPO_CORTO="equipo"
USUARIO="$(id -un 2>/dev/null || echo "${USER:-desconocido}")"
TS="$(date +%Y%m%d_%H%M%S)"
FECHA="$(date '+%Y-%m-%d %H:%M:%S')"
NOMBRE_SEGURO="$(printf '%s' "$API_HOST" | tr -c 'A-Za-z0-9.-' '_')"

DIR_SALIDA=""
if tiene xdg-user-dir; then
    d="$(xdg-user-dir DESKTOP 2>/dev/null)"
    if [ -n "$d" ] && [ "$d" != "$HOME" ] && [ -d "$d" ] && [ -w "$d" ]; then DIR_SALIDA="$d"; fi
fi
if [ -z "$DIR_SALIDA" ]; then
    for d in "$HOME/Desktop" "$HOME/Escritorio"; do
        if [ -d "$d" ] && [ -w "$d" ]; then DIR_SALIDA="$d"; break; fi
    done
fi
if [ -z "$DIR_SALIDA" ]; then
    if [ -w "." ]; then DIR_SALIDA="$(pwd)"; else DIR_SALIDA="${TMPDIR:-/tmp}"; fi
fi
OUT="$DIR_SALIDA/Diagnostico_Authentic_${NOMBRE_SEGURO}_${TS}.txt"

TMPD="$(mktemp -d 2>/dev/null || mktemp -d -t authdiag 2>/dev/null)"
if [ -z "$TMPD" ] || [ ! -d "$TMPD" ]; then
    TMPD="${TMPDIR:-/tmp}/authdiag_$$"
    mkdir -p "$TMPD" 2>/dev/null
fi
limpiar() { [ -n "$TMPD" ] && rm -rf "$TMPD" 2>/dev/null; }
trap limpiar EXIT
trap 'echo; echo "Diagnostico interrumpido. Informe parcial: $OUT"; limpiar; exit 130' INT TERM

# ---------------------------------------------------------------------
# Escritura de resultados
# ---------------------------------------------------------------------
N_OK=0; N_OBS=0; N_FALLA=0
L_OBS=""; L_FALLA=""
SUBRAYA="---"

W() { printf '%s\n' "$*" >> "$OUT"; }

resultado() {
    printf '%s %s\n' "$1" "$3" >> "$OUT"
    printf '%b%s%b %s\n' "$2" "$1" "$C_RST" "$3"
}
r_ok()    { N_OK=$((N_OK + 1)); resultado "[OK]" "$C_VERDE" "$1"; }
r_obs()   { N_OBS=$((N_OBS + 1)); L_OBS="${L_OBS}$1${NL}"; resultado "[OBS]" "$C_AMAR" "$1"; }
r_falla() { N_FALLA=$((N_FALLA + 1)); L_FALLA="${L_FALLA}$1${NL}"; resultado "[FALLA]" "$C_ROJO" "$1"; }
r_info()  { resultado "[INFO]" "$C_GRIS" "$1"; }

etapa() {
    W ""
    W "[$1] $2"
    W "$SUBRAYA"
    printf '\n%b[%s/12] %s...%b\n' "$C_CIAN" "$1" "$2" "$C_RST"
}
sub() { W ""; W "  $1:"; }
progreso() { printf '%s' "$1"; }

# Bloque de texto con sangria al informe
W_bloque() {
    local pre="${2:-    }"
    printf '%s\n' "$1" | while IFS= read -r l; do
        printf '%s%s\n' "$pre" "$l"
    done >> "$OUT"
}

# Variables para el resumen
CPU_MODELO=""; CPU_NUCLEOS=""; RAM_TOTAL_GB=""; RAM_DISP_GB=""; GPU_PRINCIPAL=""
DISCOS_SSD=0; DISCOS_HDD=0; DISCOS_TOTAL=0
GATEWAY=""; IFACE_DEF=""; MTU_IFACE=""; VPN_ON=0; VPN_LISTA=""; WIFI_DEBIL=0
API_IPS=""; API_IP=""; DNS_RES=""; DNS_LENTO=0; DNS_OMITIDO=0
LAT_SRV=""; LAT_GW=""; LAT_METODO=""; JIT_SRV=""; PERD_SRV=""; ICMP_BLOQ=0
MTU_OPT=""; HTTP_VER=""; HTTP11=0; HTTP_TOT=""; PROXY_ON=0; PUERTO_CERRADO=0
CERT_MAL=0; CERT_DIAS=""; RELOJ_DESF=0; API_CAIDA=0; LIMITE_429=0; MEM_BAJA=0
DISCO_LLENO=0; UPTIME_LARGO=0; TEMP_ALTA=0; LAT_ALTA=0; JIT_ALTO=0; PERDIDA=0
UPTIME_DIAS=""; SALTOS=""; P95_HTTP=""; TCP_API_OK=""; WIFI_PCT=""

# =====================================================================
# Encabezado
# =====================================================================
: > "$OUT" 2>/dev/null || { OUT="${TMPDIR:-/tmp}/Diagnostico_Authentic_${NOMBRE_SEGURO}_${TS}.txt"; : > "$OUT"; }
W "================================================================"
W " DIAGNOSTICO DE EQUIPO CLIENTE - AUTHENTIC (2Tec Sistemas)"
W "================================================================"
W " Fecha: ${FECHA}   Plataforma: ${PLATAFORMA}"
W " Equipo: ${EQUIPO}   Usuario: ${USUARIO}"
W " API: ${API_URL}   Ambiente: ${AMBIENTE}"
W " Version del script: ${SCRIPT_VERSION}"
W "================================================================"

printf '%b================================================================%b\n' "$C_MAG" "$C_RST"
printf '%b DIAGNOSTICO DE EQUIPO CLIENTE - AUTHENTIC (2Tec Sistemas)%b\n' "$C_MAG" "$C_RST"
printf '%b================================================================%b\n' "$C_MAG" "$C_RST"
echo " Plataforma: $PLATAFORMA   Equipo: $EQUIPO"
echo " API: $API_URL   Ambiente: $AMBIENTE"
echo " Informe: $OUT"
echo " Duracion estimada: 3 a 6 minutos. Ningun dato sale de este equipo."

# =====================================================================
# [1] HARDWARE
# =====================================================================
hw_cpu_mac() {
    local marca fis log fmax l2 l3 arq hv pc ec rosetta
    marca="$(sysctl -n machdep.cpu.brand_string 2>/dev/null)"
    fis="$(sysctl -n hw.physicalcpu 2>/dev/null)"
    log="$(sysctl -n hw.logicalcpu 2>/dev/null)"
    fmax="$(sysctl -n hw.cpufrequency_max 2>/dev/null)"
    l2="$(sysctl -n hw.l2cachesize 2>/dev/null)"
    [ -z "$l2" ] && l2="$(sysctl -n hw.perflevel0.l2cachesize 2>/dev/null)"
    l3="$(sysctl -n hw.l3cachesize 2>/dev/null)"
    arq="$(uname -m 2>/dev/null)"
    hv="$(sysctl -n kern.hv_support 2>/dev/null)"
    pc="$(sysctl -n hw.perflevel0.physicalcpu 2>/dev/null)"
    ec="$(sysctl -n hw.perflevel1.physicalcpu 2>/dev/null)"
    rosetta="$(sysctl -n sysctl.proc_translated 2>/dev/null)"
    CPU_MODELO="${marca:-$ND}"; CPU_NUCLEOS="$fis"
    W "    Modelo: ${marca:-$ND}"
    W "    Arquitectura: ${arq:-$ND}"
    W "    Nucleos fisicos: ${fis:-$ND}"
    [ -n "$pc" ] && [ -n "$ec" ] && W "    Nucleos de rendimiento/eficiencia: $pc / $ec"
    W "    Nucleos logicos: ${log:-$ND}"
    if [ -n "$fmax" ]; then W "    Velocidad maxima: $((fmax / 1000000)) MHz"; else W "    Velocidad maxima: $ND (no informada en Apple Silicon)"; fi
    [ -n "$l2" ] && W "    Cache L2: $(awk -v b="$l2" 'BEGIN{printf "%.2f", b/1048576}') MB"
    [ -n "$l3" ] && W "    Cache L3: $(awk -v b="$l3" 'BEGIN{printf "%.2f", b/1048576}') MB"
    case "$hv" in 1) W "    Virtualizacion: Habilitada" ;; 0) W "    Virtualizacion: Deshabilitada" ;; *) W "    Virtualizacion: $ND" ;; esac
    [ "$rosetta" = "1" ] && W "    Nota: el terminal se ejecuta bajo Rosetta 2 (emulacion x86_64)"
}

hw_cpu_linux() {
    local lc modelo arq cps soc logicos fmax fact l2 l3 virt fab
    if tiene lscpu; then
        lc="$(run_to 10 lscpu 2>/dev/null)"
    fi
    if [ -n "$lc" ]; then
        modelo="$(kv "$lc" "Model name")"
        fab="$(kv "$lc" "Vendor ID")"
        arq="$(kv "$lc" "Architecture")"
        cps="$(kv "$lc" "Core(s) per socket")"
        soc="$(kv "$lc" "Socket(s)")"
        [ -z "$soc" ] && soc="$(kv "$lc" "Cluster(s)")"
        logicos="$(kv "$lc" "CPU(s)")"
        fmax="$(kv "$lc" "CPU max MHz")"
        fact="$(kv "$lc" "CPU MHz")"
        l2="$(kv "$lc" "L2 cache")"
        l3="$(kv "$lc" "L3 cache")"
        virt="$(kv "$lc" "Virtualization")"
    fi
    if [ -r /proc/cpuinfo ]; then
        [ -z "$modelo" ] && modelo="$(awk -F': *' '/^(model name|Hardware|Processor|cpu model)[ \t]*:/{print $2; exit}' /proc/cpuinfo)"
        [ -z "$logicos" ] && logicos="$(grep -c '^processor' /proc/cpuinfo 2>/dev/null)"
        [ -z "$fact" ] && fact="$(awk -F': *' '/^cpu MHz/{print $2; exit}' /proc/cpuinfo)"
        if [ -z "$cps" ]; then
            cps="$(awk -F': *' '/^cpu cores/{print $2; exit}' /proc/cpuinfo)"
            soc="$(awk -F': *' '/^physical id/{print $2}' /proc/cpuinfo | sort -u | wc -l | tr -d ' ')"
            [ "$soc" = "0" ] && soc=1
        fi
        [ -z "$fab" ] && fab="$(awk -F': *' '/^vendor_id/{print $2; exit}' /proc/cpuinfo)"
        if [ -z "$virt" ]; then
            if grep -qw vmx /proc/cpuinfo 2>/dev/null; then virt="VT-x"; elif grep -qw svm /proc/cpuinfo 2>/dev/null; then virt="AMD-V"; fi
        fi
    fi
    [ -z "$logicos" ] && tiene nproc && logicos="$(nproc 2>/dev/null)"
    [ -z "$arq" ] && arq="$(uname -m 2>/dev/null)"
    local fis=""
    local ca="${cps%%[!0-9]*}" cb="${soc%%[!0-9]*}"
    if [ -n "$ca" ] && [ -n "$cb" ]; then fis=$((ca * cb)); fi
    [ -z "$fis" ] || [ "$fis" = "0" ] && fis="$logicos"
    CPU_MODELO="${modelo:-$ND}"; CPU_NUCLEOS="$fis"
    W "    Modelo: ${modelo:-$ND}"
    W "    Fabricante: ${fab:-$ND}"
    W "    Arquitectura: ${arq:-$ND}"
    W "    Nucleos fisicos: ${fis:-$ND}"
    W "    Nucleos logicos: ${logicos:-$ND}"
    if [ -n "$fmax" ]; then W "    Velocidad maxima: ${fmax%%.*} MHz"; else W "    Velocidad maxima: $ND"; fi
    if [ -n "$fact" ]; then W "    Velocidad actual: ${fact%%.*} MHz"; else W "    Velocidad actual: $ND"; fi
    W "    Cache L2: ${l2:-$ND}"
    W "    Cache L3: ${l3:-$ND}"
    W "    Virtualizacion: ${virt:-No detectada}"
}

hw_gpu_mac() {
    local sp
    sp="$(run_to 30 system_profiler SPDisplaysDataType 2>/dev/null)"
    if [ -z "$sp" ]; then W "    $ND"; return 1; fi
    printf '%s\n' "$sp" | awk '
        { l = $0; sub(/^[ \t]+/, "", l); v = l; sub(/^[^:]*:[ \t]*/, "", v) }
        l ~ /^Chipset Model:/          { n++; print "    GPU #" n ": " v; next }
        l ~ /^Vendor:/                 { print "      Fabricante: " v; next }
        l ~ /^VRAM/                    { print "      VRAM: " v; next }
        l ~ /^Total Number of Cores:/  { print "      Nucleos GPU: " v; next }
        l ~ /^Metal/                   { print "      Metal: " v; next }
        l ~ /^(Resolution|UI Looks like):/ { print "      Resolucion: " v; next }
        l ~ /^Display Type:/           { print "      Pantalla: " v; next }
    ' >> "$OUT"
    GPU_PRINCIPAL="$(kv "$sp" "Chipset Model")"
    return 0
}

hw_gpu_linux() {
    local lista="" l slot drv
    if tiene lspci; then
        lista="$(run_to 10 lspci 2>/dev/null | grep -Ei 'vga|3d controller|display controller')"
    fi
    if [ -n "$lista" ]; then
        while IFS= read -r l; do
            [ -z "$l" ] && continue
            slot="${l%% *}"
            W "    Modelo: ${l#* }"
            drv="$(run_to 5 lspci -k -s "$slot" 2>/dev/null | awk -F': ' '/Kernel driver in use/{print $2; exit}')"
            W "      Driver: ${drv:-$ND}"
            [ -z "$GPU_PRINCIPAL" ] && GPU_PRINCIPAL="$(printf '%s' "${l#* }" | sed 's/^[^:]*: //')"
        done <<EOF
$lista
EOF
    else
        local c n=0
        for c in /sys/class/drm/card[0-9]; do
            [ -d "$c/device" ] || continue
            n=$((n + 1))
            W "    GPU #$n: vendor $(leer "$c/device/vendor") device $(leer "$c/device/device") (lspci no disponible)"
        done
        [ "$n" -eq 0 ] && W "    $ND (lspci no instalado)"
    fi
    if tiene nvidia-smi; then
        local nv
        nv="$(run_to 10 nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>/dev/null)"
        if [ -n "$nv" ]; then W "    NVIDIA (nvidia-smi): $nv"; [ -z "$GPU_PRINCIPAL" ] && GPU_PRINCIPAL="${nv%%,*}"; fi
    fi
    if [ -n "$DISPLAY" ] && tiene xrandr; then
        local res
        res="$(run_to 5 xrandr --current 2>/dev/null | awk '/\*/{print $1 " @ " $2; exit}' | tr -d '*+')"
        [ -n "$res" ] && W "    Resolucion actual: $res Hz"
    fi
    [ -n "$lista" ] || [ -d /sys/class/drm ] || [ -n "$GPU_PRINCIPAL" ]
}

hw_ram() {
    local tot_b="" disp_b="" pagina libres inact spec
    if [ "$OS" = "mac" ]; then
        tot_b="$(sysctl -n hw.memsize 2>/dev/null)"
        local vm
        vm="$(vm_stat 2>/dev/null)"
        if [ -n "$vm" ]; then
            pagina="$(printf '%s\n' "$vm" | sed -n 's/.*page size of \([0-9]*\) bytes.*/\1/p')"
            [ -z "$pagina" ] && pagina=4096
            libres="$(printf '%s\n' "$vm" | awk -F': *' '/^Pages free/{gsub(/\./,"",$2); print $2}')"
            inact="$(printf '%s\n' "$vm" | awk -F': *' '/^Pages inactive/{gsub(/\./,"",$2); print $2}')"
            spec="$(printf '%s\n' "$vm" | awk -F': *' '/^Pages speculative/{gsub(/\./,"",$2); print $2}')"
            disp_b="$(awk -v a="${libres:-0}" -v b="${inact:-0}" -v c="${spec:-0}" -v p="$pagina" 'BEGIN{printf "%.0f", (a+b+c)*p}')"
        fi
    elif [ -r /proc/meminfo ]; then
        tot_b="$(awk '/^MemTotal:/{printf "%.0f", $2*1024}' /proc/meminfo)"
        disp_b="$(awk '/^MemAvailable:/{printf "%.0f", $2*1024}' /proc/meminfo)"
        [ -z "$disp_b" ] && disp_b="$(awk '/^(MemFree|Buffers|Cached):/{s+=$2} END{printf "%.0f", s*1024}' /proc/meminfo)"
    fi
    if [ -z "$tot_b" ] || [ "$tot_b" = "0" ]; then
        W "    Memoria: $ND"
        r_info "No se pudo obtener informacion de memoria"
        return
    fi
    RAM_TOTAL_GB="$(awk -v b="$tot_b" 'BEGIN{printf "%.2f", b/1073741824}')"
    RAM_DISP_GB="$(awk -v b="${disp_b:-0}" 'BEGIN{printf "%.2f", b/1073741824}')"
    local uso_gb pct
    uso_gb="$(awk -v t="$RAM_TOTAL_GB" -v d="$RAM_DISP_GB" 'BEGIN{printf "%.2f", t-d}')"
    pct="$(awk -v t="$RAM_TOTAL_GB" -v u="$uso_gb" 'BEGIN{ if (t>0) printf "%.2f", u/t*100; else print 0 }')"
    W "    Total instalada: $RAM_TOTAL_GB GB"
    W "    En uso: $uso_gb GB ($pct%)"
    W "    Disponible: $RAM_DISP_GB GB"
    if [ "$OS" = "mac" ] && tiene memory_pressure; then
        local mp
        mp="$(run_to 10 memory_pressure 2>/dev/null | sed -n 's/.*free percentage: *\([0-9]*\)%.*/\1/p')"
        [ -n "$mp" ] && W "    Memoria libre segun memory_pressure: $mp%"
    fi
    if [ -r /proc/meminfo ]; then
        local st sf
        st="$(awk '/^SwapTotal:/{printf "%.2f", $2/1048576}' /proc/meminfo)"
        sf="$(awk '/^SwapFree:/{printf "%.2f", $2/1048576}' /proc/meminfo)"
        [ -n "$st" ] && W "    Swap: $sf GB libres de $st GB"
    fi
    W ""
    W "    Modulos de memoria:"
    if [ "$OS" = "mac" ]; then
        local m
        m="$(run_to 30 system_profiler SPMemoryDataType 2>/dev/null | awk '
            { l = $0; sub(/^[ \t]+/, "", l) }
            l ~ /^(BANK|DIMM|ChannelA|ChannelB).*:$/ { print "      " l; next }
            l ~ /^(Memory|Size|Type|Speed|Manufacturer|Part Number):/ { print "        " l }')"
        if [ -n "$m" ]; then printf '%s\n' "$m" >> "$OUT"; else W "      $ND"; fi
    else
        local dm=""
        tiene dmidecode && dm="$(run_to 10 dmidecode -t 17 2>/dev/null)"
        if printf '%s' "$dm" | grep -q 'Size:'; then
            printf '%s\n' "$dm" | awk '
                /^Memory Device/ { n++; print "      Slot " n ":" }
                { l = $0; sub(/^[ \t]+/, "", l) }
                l ~ /^(Size|Speed|Manufacturer|Part Number|Type):/ { print "        " l }' >> "$OUT"
        else
            W "      $ND (requiere privilegios de administrador: dmidecode)"
        fi
    fi
    if flt "$RAM_DISP_GB" 1; then
        r_falla "MEMORIA CRITICA: Menos de 1 GB disponible ($RAM_DISP_GB GB)"; MEM_BAJA=1
    elif flt "$RAM_DISP_GB" 2; then
        r_obs "Memoria baja: Solo $RAM_DISP_GB GB disponibles"; MEM_BAJA=1
    elif flt "$RAM_TOTAL_GB" 7.5; then
        r_obs "Total de RAM menor a 8 GB ($RAM_TOTAL_GB GB) puede limitar rendimiento"
    else
        r_ok "Memoria RAM adecuada ($RAM_TOTAL_GB GB total, $RAM_DISP_GB GB disponibles)"
    fi
}

hw_disco_registrar() { # $1 tipo SSD|HDD|"" ; $2 descripcion
    DISCOS_TOTAL=$((DISCOS_TOTAL + 1))
    case "$1" in
        SSD) DISCOS_SSD=$((DISCOS_SSD + 1)); r_ok "Disco SSD detectado (mejor rendimiento): $2" ;;
        HDD) DISCOS_HDD=$((DISCOS_HDD + 1)); r_info "Disco HDD detectado (rendimiento estandar): $2" ;;
    esac
}

hw_discos() {
    if [ "$OS" = "mac" ] && tiene diskutil; then
        local discos dsk info nom tam ssd prot smart ubic n=0
        discos="$(run_to 15 diskutil list physical 2>/dev/null | awk '/^\/dev\/disk/{print $1}')"
        [ -z "$discos" ] && discos="$(run_to 15 diskutil list 2>/dev/null | awk '/^\/dev\/disk.*physical/{print $1}')"
        for dsk in $discos; do
            info="$(run_to 15 diskutil info "$dsk" 2>/dev/null)"
            [ -z "$info" ] && continue
            n=$((n + 1))
            nom="$(kv "$info" "Device / Media Name")"
            tam="$(kv "$info" "Disk Size")"; tam="${tam%% (*}"
            ssd="$(kv "$info" "Solid State")"
            prot="$(kv "$info" "Protocol")"
            smart="$(kv "$info" "SMART Status")"
            ubic="$(kv "$info" "Device Location")"
            W ""
            W "    Disco $n ($dsk):"
            W "      Modelo: ${nom:-$ND}"
            W "      Tamano: ${tam:-$ND}"
            W "      Tipo: $( [ "$ssd" = "Yes" ] && echo SSD || { [ "$ssd" = "No" ] && echo HDD || echo "$ND"; } )"
            W "      Bus: ${prot:-$ND}   Ubicacion: ${ubic:-$ND}"
            W "      Estado SMART: ${smart:-$ND}"
            if [ "$ssd" = "Yes" ]; then hw_disco_registrar SSD "${nom:-$dsk}"
            elif [ "$ssd" = "No" ]; then hw_disco_registrar HDD "${nom:-$dsk}"
            else hw_disco_registrar "" ""; fi
            case "$smart" in ""|Verified|"Not Supported") ;; *) r_obs "Disco $dsk con estado SMART: $smart" ;; esac
        done
        [ "$n" -eq 0 ] && W "    Discos fisicos: $ND"
    elif tiene lsblk && lsblk -dnP -o NAME,TYPE,ROTA,TRAN,SIZE,MODEL >/dev/null 2>&1; then
        local l nom tipo rota tran tam mod n=0
        while IFS= read -r l; do
            [ -z "$l" ] && continue
            tipo="$(printf '%s' "$l" | sed -n 's/.*TYPE="\([^"]*\)".*/\1/p')"
            [ "$tipo" = "disk" ] || continue
            nom="$(printf '%s' "$l" | sed -n 's/^NAME="\([^"]*\)".*/\1/p')"
            case "$nom" in loop*|ram*|zram*|sr*|fd*) continue ;; esac
            rota="$(printf '%s' "$l" | sed -n 's/.*ROTA="\([^"]*\)".*/\1/p')"
            tran="$(printf '%s' "$l" | sed -n 's/.*TRAN="\([^"]*\)".*/\1/p')"
            tam="$(printf '%s' "$l" | sed -n 's/.*SIZE="\([^"]*\)".*/\1/p')"
            mod="$(printf '%s' "$l" | sed -n 's/.*MODEL="\([^"]*\)".*/\1/p' | sed 's/\\x20/ /g; s/[ ]*$//')"
            n=$((n + 1))
            W ""
            W "    Disco $n (/dev/$nom):"
            W "      Modelo: ${mod:-$ND}"
            W "      Tamano: ${tam:-$ND}"
            W "      Tipo: $( [ "$rota" = "0" ] && echo SSD || echo HDD )"
            W "      Bus: ${tran:-$ND}"
            if [ "$rota" = "0" ]; then hw_disco_registrar SSD "${mod:-/dev/$nom}"; else hw_disco_registrar HDD "${mod:-/dev/$nom}"; fi
        done <<EOF
$(run_to 10 lsblk -dnP -o NAME,TYPE,ROTA,TRAN,SIZE,MODEL 2>/dev/null)
EOF
        [ "$n" -eq 0 ] && W "    Discos fisicos: $ND"
    elif [ -d /sys/block ]; then
        local b nom rota sect mod n=0
        for b in /sys/block/*; do
            nom="${b##*/}"
            case "$nom" in loop*|ram*|zram*|sr*|fd*|dm-*|md*) continue ;; esac
            [ -r "$b/queue/rotational" ] || continue
            n=$((n + 1))
            rota="$(leer "$b/queue/rotational")"
            sect="$(leer "$b/size")"
            mod="$(leer "$b/device/model" | sed 's/[ ]*$//')"
            W ""
            W "    Disco $n (/dev/$nom):"
            W "      Modelo: ${mod:-$ND}"
            W "      Tamano: $(awk -v s="${sect:-0}" 'BEGIN{printf "%.2f GB", s*512/1073741824}')"
            W "      Tipo: $( [ "$rota" = "0" ] && echo SSD || echo HDD )"
            if [ "$rota" = "0" ]; then hw_disco_registrar SSD "${mod:-/dev/$nom}"; else hw_disco_registrar HDD "${mod:-/dev/$nom}"; fi
        done
        [ "$n" -eq 0 ] && W "    Discos fisicos: $ND"
    else
        W "    Discos fisicos: $ND"
    fi

    W ""
    W "    Espacio en disco por volumen:"
    local dfo l fs tot disp mnt pct n=0
    dfo="$(run_to 15 df -kP 2>/dev/null)"
    while IFS= read -r l; do
        [ -z "$l" ] && continue
        fs="$(printf '%s' "$l" | awk '{print $1}')"
        case "$fs" in
            /dev/loop*) continue ;;
            /dev/*) ;;
            [A-Za-z]:*) [ "$OS" = "otro" ] || continue ;;
            *) continue ;;
        esac
        tot="$(printf '%s' "$l" | awk '{print $2}')"
        disp="$(printf '%s' "$l" | awk '{print $4}')"
        mnt="$(printf '%s' "$l" | awk '{ m = $6; for (i = 7; i <= NF; i++) m = m " " $i; print m }')"
        case "$mnt" in
            /System/Volumes/VM|/System/Volumes/Preboot|/System/Volumes/Update*|/System/Volumes/xarts|/System/Volumes/iSCPreboot|/System/Volumes/Hardware|/private/var/vm|/boot/efi|/Library/Developer/*) continue ;;
        esac
        [ "${tot:-0}" -gt 0 ] 2>/dev/null || continue
        n=$((n + 1))
        pct="$(awk -v d="$disp" -v t="$tot" 'BEGIN{printf "%.2f", d/t*100}')"
        W "      $mnt ($fs) - $(awk -v d="$disp" 'BEGIN{printf "%.2f", d/1048576}') GB libres de $(awk -v t="$tot" 'BEGIN{printf "%.2f", t/1048576}') GB ($pct% libre)"
        if flt "$pct" 10; then r_obs "Volumen $mnt con poco espacio libre (<10%: $pct%)"; DISCO_LLENO=1; fi
    done <<EOF
$(printf '%s\n' "$dfo" | sed '1d')
EOF
    [ "$n" -eq 0 ] && W "      $ND"
}

hw_placa() {
    if [ "$OS" = "mac" ]; then
        local hw
        hw="$(run_to 30 system_profiler SPHardwareDataType 2>/dev/null)"
        if [ -z "$hw" ]; then W "    $ND"; r_info "No se pudo obtener informacion del equipo"; return; fi
        W "    Equipo:"
        W "      Modelo: $(kv "$hw" "Model Name") ($(kv "$hw" "Model Identifier"))"
        local mn chip
        mn="$(kv "$hw" "Model Number")"; [ -n "$mn" ] && W "      Numero de modelo: $mn"
        chip="$(kv "$hw" "Chip")"; [ -z "$chip" ] && chip="$(kv "$hw" "Processor Name")"
        W "      Procesador/Chip: ${chip:-$ND}"
        W "      Numero de serie: $(kv "$hw" "Serial Number (system)")"
        W "    Firmware:"
        local fw
        fw="$(kv "$hw" "System Firmware Version")"; [ -z "$fw" ] && fw="$(kv "$hw" "Boot ROM Version")"
        W "      Version del firmware: ${fw:-$ND}"
        local smc; smc="$(kv "$hw" "SMC Version (system)")"; [ -n "$smc" ] && W "      Version SMC: $smc"
        local osl; osl="$(kv "$hw" "OS Loader Version")"; [ -n "$osl" ] && W "      OS Loader: $osl"
        r_ok "Informacion de placa/firmware obtenida"
    elif [ -d /sys/class/dmi/id ]; then
        local d=/sys/class/dmi/id
        W "    Equipo: $(leer $d/sys_vendor) $(leer $d/product_name) $(leer $d/product_version)"
        W "    Placa madre:"
        W "      Fabricante: $(leer $d/board_vendor)"
        W "      Modelo: $(leer $d/board_name)"
        W "      Version: $(leer $d/board_version)"
        W "      Numero de serie: $(leer $d/board_serial 2>/dev/null || true)$( [ -r $d/board_serial ] || echo "No disponible (requiere privilegios)")"
        W "    BIOS/UEFI:"
        W "      Fabricante: $(leer $d/bios_vendor)"
        W "      Version: $(leer $d/bios_version)"
        W "      Fecha: $(leer $d/bios_date)"
        [ -d /sys/firmware/efi ] && W "      Modo de arranque: UEFI" || W "      Modo de arranque: BIOS heredado"
        r_ok "Informacion de placa madre obtenida"
    elif [ -r /proc/device-tree/model ]; then
        W "    Modelo: $(tr -d '\0' < /proc/device-tree/model)"
        r_ok "Informacion de placa obtenida (device-tree)"
    else
        W "    $ND"
        r_info "Informacion de placa madre/BIOS no disponible en esta plataforma"
    fi
}

etapa_1() {
    etapa 1 "Hardware"
    local so=""
    if [ "$OS" = "mac" ]; then
        so="macOS $(sw_vers -productVersion 2>/dev/null) ($(sw_vers -buildVersion 2>/dev/null))"
    elif [ -r /etc/os-release ]; then
        so="$(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-$NAME}")"
    fi
    W "  Sistema operativo: ${so:-$UNAME_S} - kernel $(uname -r 2>/dev/null)"

    sub "Procesador (CPU)"
    if [ "$OS" = "mac" ]; then hw_cpu_mac; else hw_cpu_linux; fi
    local c="${CPU_NUCLEOS%%[!0-9]*}"
    if [ -z "$c" ]; then
        r_info "No se pudo determinar la cantidad de nucleos"
    elif [ "$c" -lt 2 ]; then
        r_obs "CPU con pocos nucleos (<2) puede afectar rendimiento"
    elif [ "$c" -ge 8 ]; then
        r_ok "CPU con excelente cantidad de nucleos (8+)"
    else
        r_ok "CPU con cantidad adecuada de nucleos ($c)"
    fi

    sub "Tarjeta de video (GPU)"
    if [ "$OS" = "mac" ]; then hw_gpu_mac; else hw_gpu_linux; fi
    if [ $? -eq 0 ]; then r_ok "GPU(s) detectada(s)"; else r_info "Informacion de GPU no disponible"; fi

    sub "Memoria RAM"
    hw_ram

    sub "Almacenamiento"
    hw_discos

    sub "Placa madre y firmware"
    hw_placa
}

# =====================================================================
# [2] DISPOSITIVOS CONECTADOS
# =====================================================================
nota_vid() {
    case "$(minus "$1")" in
        *2341*) echo "VID de Arduino oficial" ;;
        *1a86*) echo "Chip CH340 (comun en clones Arduino)" ;;
        *10c4*) echo "Silicon Labs CP210x" ;;
        *0403*) echo "FTDI USB Serial" ;;
        *067b*) echo "Prolific USB Serial" ;;
    esac
}

clasificar_serial() {
    case "$(minus "$1")" in
        *arduino*|*ch340*|*ch341*|*cp210*|*ft232*|*usbmodem*|*wchusb*|*slab_usb*|*ttyacm*) echo "Microcontrolador/Arduino" ;;
        *barcode*|*scanner*|*symbol*|*honeywell*|*datalogic*|*zebra*) echo "Lector de Codigo de Barras" ;;
        *thermal*|*receipt*|*epson*|*"star micronics"*|*bixolon*|*" pos"*|*"pos "*|*pos-*) echo "Impresora Termica/POS" ;;
        *usbserial*|*usb-serial*|*"usb to serial"*|*prolific*|*ftdi*|*pl2303*|*ttyusb*) echo "Adaptador USB-Serial" ;;
        *bluetooth*) echo "Puerto Serial Bluetooth" ;;
        *virtual*|*debug-console*) echo "Puerto Virtual" ;;
        *) echo "Dispositivo Serial Generico" ;;
    esac
}

disp_usb() {
    local n=0 l nombre vid pid
    if [ "$OS" = "mac" ]; then
        local sp lista
        sp="$(run_to 30 system_profiler SPUSBDataType 2>/dev/null)"
        lista="$(printf '%s\n' "$sp" | awk '
            { l = $0; sub(/^[ \t]+/, "", l); sub(/[ \t]+$/, "", l) }
            l ~ /:$/ && l !~ /: / { nombre = substr(l, 1, length(l) - 1); next }
            l ~ /Product ID:/ { v = l; sub(/.*ID:[ \t]*/, "", v); split(v, a, " "); pid = a[1]; next }
            l ~ /Vendor ID:/  { v = l; sub(/.*ID:[ \t]*/, "", v); split(v, a, " "); print nombre "|" a[1] "|" pid }')"
        if [ -z "$lista" ]; then
            sp="$(run_to 30 system_profiler SPUSBHostDataType 2>/dev/null)"
            lista="$(printf '%s\n' "$sp" | awk '
                { l = $0; sub(/^[ \t]+/, "", l); sub(/[ \t]+$/, "", l) }
                l ~ /:$/ && l !~ /: / { nombre = substr(l, 1, length(l) - 1); next }
                l ~ /Product ID:/ { v = l; sub(/.*ID:[ \t]*/, "", v); split(v, a, " "); pid = a[1]; next }
                l ~ /Vendor ID:/  { v = l; sub(/.*ID:[ \t]*/, "", v); split(v, a, " "); print nombre "|" a[1] "|" pid }')"
        fi
        USB_LISTA_MAC="$lista"
        while IFS='|' read -r nombre vid pid; do
            [ -z "$nombre" ] && continue
            n=$((n + 1))
            W "    - $nombre"
            W "      VID: ${vid#0x}, PID: ${pid#0x}"
        done <<EOF
$lista
EOF
    elif tiene lsusb; then
        while IFS= read -r l; do
            [ -z "$l" ] && continue
            vid="$(printf '%s' "$l" | sed -n 's/.* ID \([0-9a-fA-F]*\):\([0-9a-fA-F]*\).*/\1/p')"
            pid="$(printf '%s' "$l" | sed -n 's/.* ID \([0-9a-fA-F]*\):\([0-9a-fA-F]*\).*/\2/p')"
            nombre="$(printf '%s' "$l" | sed -n 's/.* ID [0-9a-fA-F]*:[0-9a-fA-F]* *//p')"
            case "$nombre" in *"root hub"*) continue ;; esac
            n=$((n + 1))
            W "    - ${nombre:-Dispositivo sin nombre}"
            W "      VID: $vid, PID: $pid"
        done <<EOF
$(run_to 10 lsusb 2>/dev/null)
EOF
    elif [ -d /sys/bus/usb/devices ]; then
        local d
        for d in /sys/bus/usb/devices/*; do
            [ -r "$d/idVendor" ] || continue
            case "${d##*/}" in usb*) continue ;; esac
            n=$((n + 1))
            nombre="$(leer "$d/manufacturer") $(leer "$d/product")"
            W "    - ${nombre# }"
            W "      VID: $(leer "$d/idVendor"), PID: $(leer "$d/idProduct")"
        done
    else
        W "    $ND"
        return
    fi
    if [ "$n" -gt 0 ]; then W "    Dispositivos USB activos: $n"; else W "    No se detectaron dispositivos USB (o la herramienta no esta disponible)"; fi

    W ""
    W "    Almacenamiento USB:"
    local m=0
    if [ "$OS" = "mac" ] && tiene diskutil; then
        local dsk info
        for dsk in $(run_to 15 diskutil list external physical 2>/dev/null | awk '/^\/dev\/disk/{print $1}'); do
            info="$(run_to 15 diskutil info "$dsk" 2>/dev/null)"
            m=$((m + 1))
            W "      - $(kv "$info" "Device / Media Name") - $(kv "$info" "Disk Size" | sed 's/ (.*//') ($dsk)"
        done
    elif tiene lsblk; then
        while IFS= read -r l; do
            [ -z "$l" ] && continue
            m=$((m + 1))
            W "      - $l"
        done <<EOF
$(run_to 10 lsblk -dn -o NAME,TRAN,SIZE,MODEL 2>/dev/null | awk '$2=="usb"{n=$1; s=$3; $1=$2=$3=""; sub(/^ +/,""); print $0 " - " s " (/dev/" n ")"}')
EOF
    fi
    [ "$m" -eq 0 ] && W "      No se detectaron unidades de almacenamiento USB"
}

disp_serial() {
    local n=0 c_mc=0 c_lec=0 c_imp=0 c_otr=0 tipo nota
    W "    Puertos seriales detectados:"
    if [ "$OS" = "mac" ]; then
        local p
        for p in /dev/cu.*; do
            [ -e "$p" ] || continue
            n=$((n + 1))
            tipo="$(clasificar_serial "$p")"
            W ""
            W "      ${p}:"
            W "        Tipo detectado: $tipo"
            case "$tipo" in Microcontrolador*) W "        Nota: Posible Arduino o placa de desarrollo"; c_mc=$((c_mc + 1)) ;;
                Lector*) c_lec=$((c_lec + 1)) ;; Impresora*) c_imp=$((c_imp + 1)) ;; *) c_otr=$((c_otr + 1)) ;; esac
        done
        if [ -n "$USB_LISTA_MAC" ]; then
            local nombre vid pid
            while IFS='|' read -r nombre vid pid; do
                nota="$(nota_vid "$vid")"
                [ -n "$nota" ] && W "      USB conectado: $nombre (VID ${vid#0x}) - Nota: $nota"
            done <<EOF
$USB_LISTA_MAC
EOF
        fi
    else
        local t nom dir i vid pid prod fab
        for t in /sys/class/tty/ttyUSB* /sys/class/tty/ttyACM*; do
            [ -e "$t" ] || continue
            nom="${t##*/}"
            n=$((n + 1))
            dir="$(cd -P "$t/device" 2>/dev/null && pwd)"
            vid=""; pid=""; prod=""; fab=""
            i=0
            while [ -n "$dir" ] && [ "$i" -lt 6 ]; do
                if [ -r "$dir/idVendor" ]; then
                    vid="$(leer "$dir/idVendor")"; pid="$(leer "$dir/idProduct")"
                    prod="$(leer "$dir/product")"; fab="$(leer "$dir/manufacturer")"
                    break
                fi
                dir="${dir%/*}"; i=$((i + 1))
            done
            tipo="$(clasificar_serial "$nom $prod $fab")"
            W ""
            W "      /dev/${nom}:"
            W "        Descripcion: ${fab:+$fab }${prod:-$ND}"
            [ -n "$vid" ] && W "        Hardware ID: VID_${vid}&PID_${pid}"
            W "        Tipo detectado: $tipo"
            nota="$(nota_vid "$vid")"; [ -n "$nota" ] && W "        Nota: $nota"
            [ -e "/dev/$nom" ] && [ ! -w "/dev/$nom" ] && W "        Permisos: el usuario no tiene acceso de escritura (grupo dialout/uucp)"
            case "$tipo" in Microcontrolador*) c_mc=$((c_mc + 1)) ;; Lector*) c_lec=$((c_lec + 1)) ;; Impresora*) c_imp=$((c_imp + 1)) ;; *) c_otr=$((c_otr + 1)) ;; esac
        done
        local s reales=0
        for s in /sys/class/tty/ttyS*; do
            [ -r "$s/type" ] || continue
            [ "$(leer "$s/type")" != "0" ] && reales=$((reales + 1))
        done
        [ "$reales" -gt 0 ] && W "      Puertos seriales nativos (ttyS*) con hardware: $reales"
        if [ -d /dev/serial/by-id ]; then
            W "      Identificadores (/dev/serial/by-id):"
            for s in /dev/serial/by-id/*; do [ -e "$s" ] && W "        - ${s##*/}"; done
        fi
    fi
    if [ "$n" -eq 0 ]; then
        W "      No se detectaron puertos seriales USB activos"
        W "      Nota: Los dispositivos USB-Serial deben estar conectados y con drivers instalados"
    else
        W ""
        W "      Resumen:"
        [ "$c_mc" -gt 0 ] && W "        Microcontroladores : $c_mc"
        [ "$c_lec" -gt 0 ] && W "        Lectores : $c_lec"
        [ "$c_imp" -gt 0 ] && W "        Impresoras : $c_imp"
        [ "$c_otr" -gt 0 ] && W "        Otros : $c_otr"
    fi
}

disp_audio() {
    local a=""
    if [ "$OS" = "mac" ]; then
        a="$(run_to 30 system_profiler SPAudioDataType 2>/dev/null | awk '
            { if (match($0, /^[ ]+/)) ind = RLENGTH; else ind = 0; l = $0; sub(/^[ \t]+/, "", l) }
            ind == 8 && l ~ /:$/ { print "    - " substr(l, 1, length(l) - 1) }')"
    elif tiene aplay; then
        a="$(run_to 10 aplay -l 2>/dev/null | awk '/^card [0-9]+:/{ sub(/^card [0-9]+: /, ""); print "    - " $0 }' | sort -u)"
    fi
    if [ -z "$a" ] && [ -r /proc/asound/cards ]; then
        a="$(awk -F' - ' '/^ *[0-9]+ \[/{print "    - " $2}' /proc/asound/cards)"
    fi
    if [ -n "$a" ]; then printf '%s\n' "$a" >> "$OUT"; else W "    $ND (o sin dispositivos de audio)"; fi
}

disp_entrada() {
    local tec="" pun="" otr=""
    if [ "$OS" = "mac" ]; then
        local hid
        hid="$(run_to 15 ioreg -r -d 1 -c IOHIDDevice 2>/dev/null | sed -n 's/.*"Product" = "\(.*\)".*/\1/p' | sort -u)"
        while IFS= read -r l; do
            [ -z "$l" ] && continue
            case "$(minus "$l")" in
                *keyboard*|*teclado*) tec="${tec}  - $l${NL}" ;;
                *mouse*|*trackpad*|*touch*|*pointer*) pun="${pun}  - $l${NL}" ;;
                *) otr="${otr}  - $l${NL}" ;;
            esac
        done <<EOF
$hid
EOF
    elif [ -r /proc/bus/input/devices ]; then
        local l tipo nombre
        while IFS='|' read -r tipo nombre; do
            case "$tipo" in
                T) tec="${tec}  - $nombre${NL}" ;;
                P) pun="${pun}  - $nombre${NL}" ;;
            esac
        done <<EOF
$(awk '
    /^N: Name=/ { n = $0; sub(/^N: Name="?/, "", n); sub(/"$/, "", n) }
    /^H: Handlers=/ {
        h = $0; ln = tolower(n)
        if (h ~ /mouse/) print "P|" n
        else if (h ~ /kbd/ && (ln ~ /keyboard/ || h ~ /leds/)) print "T|" n
    }' /proc/bus/input/devices)
EOF
    else
        W "    $ND"
        return
    fi
    W "    Teclados:"
    if [ -n "$tec" ]; then printf '%s' "$tec" | sed 's/^/    /' >> "$OUT"; else W "      - No se detectaron teclados (normal en sistemas remotos)"; fi
    W "    Mouse/Touchpad:"
    if [ -n "$pun" ]; then printf '%s' "$pun" | sed 's/^/    /' >> "$OUT"; else W "      - No se detectaron dispositivos de puntero (normal en sistemas remotos)"; fi
    if [ -n "$otr" ]; then
        W "    Otros dispositivos HID:"
        printf '%s' "$otr" | sed 's/^/    /' >> "$OUT"
    fi
}

etapa_2() {
    etapa 2 "Dispositivos conectados"
    USB_LISTA_MAC=""
    sub "Dispositivos USB"
    disp_usb
    sub "Puertos seriales (equivalente a COM)"
    disp_serial
    sub "Dispositivos de audio"
    disp_audio
    sub "Dispositivos de entrada"
    disp_entrada
    r_info "Inventario de dispositivos completado"
}

# =====================================================================
# [3] ESTADO DEL SISTEMA
# =====================================================================
sis_uptime() {
    local seg="" arranque=""
    if [ "$OS" = "mac" ]; then
        local bt ahora
        bt="$(sysctl -n kern.boottime 2>/dev/null | sed -n 's/.*sec = \([0-9]*\).*/\1/p')"
        ahora="$(date +%s)"
        if [ -n "$bt" ]; then seg=$((ahora - bt)); arranque="$(date -r "$bt" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)"; fi
    elif [ -r /proc/uptime ]; then
        seg="$(awk '{printf "%d", $1}' /proc/uptime)"
        arranque="$(uptime -s 2>/dev/null)"
        [ -z "$arranque" ] && arranque="$(date -d "@$(( $(date +%s) - seg ))" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)"
    fi
    if [ -z "$seg" ]; then W "    $ND"; r_info "No se pudo obtener el tiempo de actividad"; return; fi
    local d=$((seg / 86400)) h=$(((seg % 86400) / 3600)) m=$(((seg % 3600) / 60))
    W "    Sistema activo desde: ${arranque:-$ND}"
    W "    Tiempo activo: $d dias, $h horas, $m minutos"
    UPTIME_DIAS="$d"
    if [ "$d" -gt 30 ]; then r_obs "Sistema sin reiniciar por mas de 30 dias ($d dias)"; UPTIME_LARGO=1
    else r_ok "Tiempo de actividad normal ($d dias)"; fi
    local carga=""
    if [ -r /proc/loadavg ]; then carga="$(awk '{print $1", "$2", "$3}' /proc/loadavg)"
    elif [ "$OS" = "mac" ]; then carga="$(sysctl -n vm.loadavg 2>/dev/null | tr -d '{}' | awk '{print $1", "$2", "$3}')"; fi
    [ -n "$carga" ] && W "    Carga promedio (1, 5, 15 min): $carga"
}

sis_temperaturas() {
    local lista="" max="" l
    if [ "$OS" = "mac" ]; then
        local t=""
        if tiene osx-cpu-temp; then t="$(run_to 10 osx-cpu-temp 2>/dev/null | sed -n 's/^\([0-9.]*\).*/\1/p')"
        elif tiene smctemp; then t="$(run_to 10 smctemp -c 2>/dev/null | sed -n 's/^\([0-9.]*\).*/\1/p')"
        elif tiene istats; then t="$(run_to 10 istats cpu temp --value-only 2>/dev/null | sed -n 's/^[^0-9]*\([0-9.]*\).*/\1/p')"; fi
        [ -n "$t" ] && lista="CPU|$t"
        if tiene pmset; then
            local th lim
            th="$(run_to 10 pmset -g therm 2>/dev/null)"
            if [ -n "$th" ]; then
                W "    Estado termico (pmset -g therm):"
                W_bloque "$th" "      "
                lim="$(printf '%s\n' "$th" | sed -n 's/.*CPU_Speed_Limit[^0-9]*\([0-9]*\).*/\1/p' | head -1)"
                if [ -n "$lim" ] && [ "$lim" -lt 100 ]; then
                    r_obs "CPU limitada termicamente (CPU_Speed_Limit = $lim%)"; TEMP_ALTA=1
                fi
            fi
        fi
    else
        if tiene sensors; then
            lista="$(run_to 10 sensors 2>/dev/null | awk -F: 'NF >= 2 {
                r = $2
                if (match(r, /^[ \t]*[+-][0-9]+(\.[0-9]+)?/)) {
                    v = substr(r, RSTART, RLENGTH); u = substr(r, RSTART + RLENGTH, 4)
                    gsub(/[ \t+]/, "", v)
                    if (u ~ /C/) print $1 "|" v
                }
            }' | head -n 20)"
        fi
        if [ -z "$lista" ]; then
            local z tp ty
            for z in /sys/class/thermal/thermal_zone*; do
                [ -r "$z/temp" ] || continue
                tp="$(leer "$z/temp")"; ty="$(leer "$z/type")"
                case "$tp" in ''|*[!0-9-]*) continue ;; esac
                lista="${lista}${ty:-zona}|$(awk -v t="$tp" 'BEGIN{printf "%.1f", t/1000}')${NL}"
            done
        fi
    fi
    if [ -z "$lista" ]; then
        if [ "$OS" = "mac" ]; then
            r_info "Sensores de temperatura no disponibles sin privilegios en macOS (se informa el estado termico)"
        else
            r_info "Sensores de temperatura no disponibles"
        fi
        return
    fi
    local nombre val
    while IFS='|' read -r nombre val; do
        [ -z "$val" ] && continue
        W "    $nombre: ${val} C"
        if [ -z "$max" ] || fgt "$val" "$max"; then max="$val"; fi
    done <<EOF
$lista
EOF
    if [ -n "$max" ] && fgt "$max" 80; then r_obs "Temperatura alta detectada (>80 C: $max C)"; TEMP_ALTA=1
    elif [ -n "$max" ]; then r_ok "Temperaturas normales (maxima $max C)"; fi
}

sis_procesos() {
    local ps_o
    ps_o="$(run_to 15 ps -A -o pcpu= -o rss= -o comm= 2>/dev/null | awk '
        $1 ~ /^[0-9.]+$/ && $2 ~ /^[0-9]+$/ {
            c = $0; sub(/^[ \t]*[0-9.]+[ \t]+[0-9]+[ \t]+/, "", c); k = split(c, a, "/"); nm = a[k]
            printf("%s|%s|%s\n", $1, $2, nm)
        }')"
    if [ -z "$ps_o" ]; then W "    $ND"; r_info "No se pudo obtener la lista de procesos"; return; fi
    W "    Top 10 procesos (CPU):"
    printf '%s\n' "$ps_o" | sort -t'|' -k1,1 -rn | head -n 10 | awk -F'|' '{printf("      - %s: CPU=%s%%, Memoria=%.2f MB\n", $3, $1, $2/1024)}' >> "$OUT"
    W ""
    W "    Top 10 procesos (Memoria):"
    printf '%s\n' "$ps_o" | sort -t'|' -k2,2 -rn | head -n 10 | awk -F'|' '{printf("      - %s: %.2f MB\n", $3, $2/1024)}' >> "$OUT"
    local cpu_total
    cpu_total="$(printf '%s\n' "$ps_o" | awk -F'|' '{s+=$1} END{printf "%.0f", s}')"
    W "    Suma de %CPU de todos los procesos: $cpu_total%"
}

sis_servicios() {
    if [ "$OS" = "mac" ]; then
        local p
        for p in mDNSResponder configd timed; do
            if pgrep -x "$p" >/dev/null 2>&1; then
                W "    - $p: En ejecucion"
            else
                W "    - $p: No detectado"
                case "$p" in mDNSResponder|configd) r_obs "Servicio critico $p no esta ejecutandose" ;; esac
            fi
        done
    elif tiene systemctl && [ -d /run/systemd/system ]; then
        local u act hab vistos=0
        for u in NetworkManager systemd-networkd systemd-resolved wpa_supplicant dhcpcd connman systemd-timesyncd chronyd chrony ntp ntpd avahi-daemon; do
            hab="$(systemctl is-enabled "$u" 2>/dev/null)"
            [ -z "$hab" ] && continue
            case "$hab" in not-found|masked) continue ;; esac
            act="$(systemctl is-active "$u" 2>/dev/null)"
            vistos=$((vistos + 1))
            W "    - $u: $act (inicio: $hab)"
            if [ "$hab" = "enabled" ] && [ "$act" != "active" ]; then
                r_obs "Servicio $u habilitado pero no esta ejecutandose ($act)"
            fi
        done
        [ "$vistos" -eq 0 ] && W "    No se encontraron servicios de red conocidos"
    else
        W "    $ND (sin systemd/launchd)"
    fi
}

etapa_3() {
    etapa 3 "Estado del sistema"
    sub "Tiempo de actividad"
    sis_uptime
    sub "Temperaturas"
    sis_temperaturas
    sub "Procesos con mayor consumo"
    sis_procesos
    sub "Servicios relevantes"
    sis_servicios
}

# =====================================================================
# [4] RED LOCAL
# =====================================================================
es_vpn_nombre() {
    case "$1" in
        tun*|tap*|wg*|ppp*|ipsec*|vti*|zt*|tailscale*|nordlynx*|proton*|cscotun*|ham*|gpd*|utun*|outline*|mullvad*) return 0 ;;
    esac
    return 1
}

senal_pct_desde_dbm() { awk -v d="$1" 'BEGIN{ p = 2 * (d + 100); if (p > 100) p = 100; if (p < 0) p = 0; printf "%d", p }'; }

evaluar_senal() {
    local pct="$1"
    case "$pct" in ''|*[!0-9]*) return ;; esac
    W "      Senal: $pct%"; WIFI_PCT="$pct"
    if [ "$pct" -lt 50 ]; then r_obs "Senal WiFi debil (<50%: $pct%)"; WIFI_DEBIL=1
    else r_ok "Senal WiFi adecuada ($pct%)"; fi
}

wifi_mac() {
    local dev="$1" sp bloque ssid phy canal seg sn rssi tx
    sp="$(run_to 30 system_profiler SPAirPortDataType 2>/dev/null)"
    bloque="$(printf '%s\n' "$sp" | awk '
        /Current Network Information:/ { f = 1; s = 1; next }
        /Other Local Wi-Fi Networks:/ { f = 0 }
        f {
            l = $0; sub(/^[ \t]+/, "", l); sub(/[ \t]+$/, "", l)
            if (s) { sub(/:$/, "", l); print "SSID|" l; s = 0; next }
            k = l; sub(/:.*/, "", k); v = l; sub(/^[^:]*:[ \t]*/, "", v); print k "|" v
        }')"
    if [ -n "$bloque" ]; then
        ssid="$(printf '%s\n' "$bloque" | awk -F'|' '$1=="SSID"{print $2; exit}')"
        phy="$(printf '%s\n' "$bloque" | awk -F'|' '$1=="PHY Mode"{print $2; exit}')"
        canal="$(printf '%s\n' "$bloque" | awk -F'|' '$1=="Channel"{print $2; exit}')"
        seg="$(printf '%s\n' "$bloque" | awk -F'|' '$1=="Security"{print $2; exit}')"
        sn="$(printf '%s\n' "$bloque" | awk -F'|' '$1=="Signal / Noise"{print $2; exit}')"
        tx="$(printf '%s\n' "$bloque" | awk -F'|' '$1=="Transmit Rate"{print $2; exit}')"
        case "$ssid" in "<redacted>"|"") ssid="(oculto por la configuracion de privacidad de macOS)" ;; esac
        W "      SSID: $ssid"
        [ -n "$canal" ] && W "      Canal: $canal"
        [ -n "$seg" ] && W "      Autenticacion: $seg"
        [ -n "$phy" ] && W "      Tipo de radio: $phy"
        [ -n "$tx" ] && W "      Velocidad de transmision: $tx Mbps"
        if [ -n "$sn" ]; then
            W "      Senal / Ruido: $sn"
            rssi="$(printf '%s' "$sn" | sed -n 's/^\(-[0-9]*\) dBm.*/\1/p')"
            [ -n "$rssi" ] && evaluar_senal "$(senal_pct_desde_dbm "$rssi")"
        fi
        return 0
    fi
    # Respaldo: binario airport (macOS antiguos)
    local ap="/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport"
    if [ -x "$ap" ]; then
        local o; o="$(run_to 10 "$ap" -I 2>/dev/null)"
        if [ -n "$o" ] && printf '%s' "$o" | grep -q 'SSID'; then
            W "      SSID: $(printf '%s\n' "$o" | awk -F': ' '$1 ~ /^ *SSID$/{print $2; exit}')"
            W "      Canal: $(printf '%s\n' "$o" | awk -F': ' '$1 ~ /channel$/{print $2; exit}')"
            rssi="$(printf '%s\n' "$o" | awk -F': ' '$1 ~ /agrCtlRSSI$/{print $2; exit}')"
            [ -n "$rssi" ] && evaluar_senal "$(senal_pct_desde_dbm "$rssi")"
            return 0
        fi
    fi
    local nw; nw="$(run_to 10 networksetup -getairportnetwork "$dev" 2>/dev/null)"
    [ -n "$nw" ] && W "      $nw"
    return 0
}

wifi_linux() {
    local dev="$1" l ssid sig chan sec q dbm
    if tiene nmcli; then
        l="$(run_to 15 nmcli -t -f ACTIVE,SIGNAL,CHAN,SECURITY,SSID dev wifi list ifname "$dev" --rescan no 2>/dev/null | grep '^yes:' | head -1)"
        [ -z "$l" ] && l="$(run_to 15 nmcli -t -f ACTIVE,SIGNAL,CHAN,SECURITY,SSID dev wifi 2>/dev/null | grep '^yes:' | head -1)"
        if [ -n "$l" ]; then
            sig="$(printf '%s' "$l" | cut -d: -f2)"
            chan="$(printf '%s' "$l" | cut -d: -f3)"
            sec="$(printf '%s' "$l" | cut -d: -f4)"
            ssid="$(printf '%s' "$l" | cut -d: -f5- | sed 's/\\:/:/g')"
            W "      SSID: ${ssid:-$ND}"
            W "      Canal: ${chan:-$ND}"
            W "      Autenticacion: ${sec:-$ND}"
            evaluar_senal "$sig"
            return 0
        fi
    fi
    if tiene iw; then
        l="$(run_to 10 iw dev "$dev" link 2>/dev/null)"
        if printf '%s' "$l" | grep -q 'SSID'; then
            W "      SSID: $(printf '%s\n' "$l" | sed -n 's/^[[:space:]]*SSID: //p')"
            W "      Frecuencia: $(printf '%s\n' "$l" | sed -n 's/^[[:space:]]*freq: //p') MHz"
            W "      Velocidad TX: $(printf '%s\n' "$l" | sed -n 's/^[[:space:]]*tx bitrate: //p')"
            dbm="$(printf '%s\n' "$l" | sed -n 's/^[[:space:]]*signal: \(-[0-9]*\).*/\1/p')"
            [ -n "$dbm" ] && W "      Nivel de senal: $dbm dBm" && evaluar_senal "$(senal_pct_desde_dbm "$dbm")"
            return 0
        fi
    fi
    if tiene iwconfig; then
        l="$(run_to 10 iwconfig "$dev" 2>/dev/null)"
        if [ -n "$l" ]; then
            W "      SSID: $(printf '%s\n' "$l" | sed -n 's/.*ESSID:"\(.*\)".*/\1/p')"
            W "      Frecuencia: $(printf '%s\n' "$l" | sed -n 's/.*Frequency:\([0-9.]* GHz\).*/\1/p')"
            q="$(printf '%s\n' "$l" | sed -n 's/.*Link Quality=\([0-9]*\)\/\([0-9]*\).*/\1 \2/p')"
            dbm="$(printf '%s\n' "$l" | sed -n 's/.*Signal level=\(-[0-9]*\) dBm.*/\1/p')"
            [ -n "$dbm" ] && W "      Nivel de senal: $dbm dBm"
            if [ -n "$q" ]; then evaluar_senal "$(printf '%s' "$q" | awk '{ if ($2 > 0) printf "%d", $1 / $2 * 100 }')"
            elif [ -n "$dbm" ]; then evaluar_senal "$(senal_pct_desde_dbm "$dbm")"; fi
            return 0
        fi
    fi
    if [ -r /proc/net/wireless ]; then
        q="$(awk -v d="$dev:" '$1==d {gsub(/\./,"",$3); print $3}' /proc/net/wireless)"
        if [ -n "$q" ]; then evaluar_senal "$(awk -v q="$q" 'BEGIN{ p = q / 70 * 100; if (p > 100) p = 100; printf "%d", p }')"; return 0; fi
    fi
    W "      Datos WiFi: $ND (instale nmcli, iw o wireless-tools)"
}

mascara_hex_a_prefijo() {
    local h="${1#0x}" n=0 v
    case "$h" in *[!0-9a-fA-F]*|"") echo "$1"; return ;; esac
    v=$((16#$h))
    while [ "$v" -gt 0 ]; do n=$((n + (v & 1))); v=$((v >> 1)); done
    echo "$n"
}

red_mac() {
    local puertos ifs i o ips mac media estado mtu puerto n=0
    puertos="$(run_to 15 networksetup -listallhardwareports 2>/dev/null | awk -F': ' '/^Hardware Port:/{p=$2} /^Device:/{print $2 "|" p}')"
    GATEWAY="$(run_to 10 route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}')"
    IFACE_DEF="$(run_to 10 route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')"
    [ -z "$GATEWAY" ] && GATEWAY="$(run_to 10 netstat -rn -f inet 2>/dev/null | awk '$1=="default"{print $2; exit}')"
    ifs="$(ifconfig -l 2>/dev/null)"
    for i in $ifs; do
        case "$i" in lo*|gif*|stf*|anpi*|ap*|awdl*|llw*) continue ;; esac
        o="$(ifconfig "$i" 2>/dev/null)"
        ips="$(printf '%s\n' "$o" | awk '/inet /{print $2 "|" $4}')"
        estado="$(printf '%s\n' "$o" | sed -n 's/.*status: //p' | head -1)"
        [ -z "$ips" ] && [ "$estado" != "active" ] && continue
        n=$((n + 1))
        mac="$(printf '%s\n' "$o" | awk '/ether /{print $2; exit}')"
        media="$(printf '%s\n' "$o" | sed -n 's/.*media: //p' | head -1)"
        mtu="$(printf '%s\n' "$o" | sed -n 's/.* mtu \([0-9]*\).*/\1/p' | head -1)"
        puerto="$(printf '%s\n' "$puertos" | awk -F'|' -v d="$i" '$1==d{print $2; exit}')"
        W ""
        W "    Adaptador: $i${puerto:+ ($puerto)}"
        W "      Estado: ${estado:-activo}"
        [ -n "$media" ] && W "      Velocidad del enlace: $media"
        W "      MAC: ${mac:-$ND}"
        W "      MTU: ${mtu:-$ND}"
        local ip msk
        while IFS='|' read -r ip msk; do
            [ -z "$ip" ] && continue
            W "      IPv4: $ip/$(mascara_hex_a_prefijo "$msk")"
        done <<EOF
$ips
EOF
        [ "$i" = "$IFACE_DEF" ] && { W "      Gateway: ${GATEWAY:-$ND} (ruta por defecto)"; MTU_IFACE="$mtu"; }
        if [ -n "$ips" ] && es_vpn_nombre "$i"; then
            VPN_ON=1; VPN_LISTA="$VPN_LISTA $i"
            r_obs "INTERFAZ VPN DETECTADA: $i"
        fi
        case "$puerto" in
            Wi-Fi|AirPort) wifi_mac "$i" ;;
        esac
    done
    [ "$n" -eq 0 ] && W "    No se detectaron adaptadores activos"
    local dns
    dns="$(run_to 10 scutil --dns 2>/dev/null | awk '/nameserver\[[0-9]+\]/{print $3}' | awk '!s[$0]++' | tr '\n' ' ')"
    W ""
    W "    Servidores DNS configurados: ${dns:-$ND}"
}

red_linux() {
    local n=0 i ips mac mtu vel est drv ruta metrica
    if tiene ip; then
        ruta="$(ip route show default 2>/dev/null | head -1)"
        GATEWAY="$(printf '%s' "$ruta" | awk '{for(i=1;i<=NF;i++) if($i=="via"){print $(i+1); exit}}')"
        IFACE_DEF="$(printf '%s' "$ruta" | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')"
        metrica="$(printf '%s' "$ruta" | awk '{for(i=1;i<=NF;i++) if($i=="metric"){print $(i+1); exit}}')"
    elif [ -r /proc/net/route ]; then
        local l
        l="$(awk 'NR>1 && $2=="00000000" {print $1, $3, $7; exit}' /proc/net/route)"
        if [ -n "$l" ]; then
            IFACE_DEF="${l%% *}"
            local hx; hx="$(printf '%s' "$l" | awk '{print $2}')"
            case "$hx" in
                [0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])
                    GATEWAY="$((16#${hx:6:2})).$((16#${hx:4:2})).$((16#${hx:2:2})).$((16#${hx:0:2}))" ;;
            esac
            metrica="$(printf '%s' "$l" | awk '{print $3}')"
        fi
    fi
    local lista_if=""
    if tiene ip; then
        lista_if="$(ip -o link show 2>/dev/null | awk -F': ' '{ n = $2; sub(/@.*/, "", n); if ($3 ~ /[<,]UP[,>]/ && $3 ~ /LOWER_UP/) print n }')"
    elif [ -d /sys/class/net ]; then
        for i in /sys/class/net/*; do
            [ "$(leer "$i/operstate")" = "up" ] || [ "$(leer "$i/operstate")" = "unknown" ] && lista_if="$lista_if ${i##*/}"
        done
    fi
    for i in $lista_if; do
        [ "$i" = "lo" ] && continue
        n=$((n + 1))
        mac="$(leer "/sys/class/net/$i/address")"
        mtu="$(leer "/sys/class/net/$i/mtu")"
        vel="$(cat "/sys/class/net/$i/speed" 2>/dev/null)"
        est="$(leer "/sys/class/net/$i/operstate")"
        drv="$(cd -P "/sys/class/net/$i/device/driver" 2>/dev/null && basename "$(pwd)")"
        if tiene ip; then
            ips="$(ip -o -4 addr show dev "$i" 2>/dev/null | awk '{print $4}')"
        elif tiene ifconfig; then
            ips="$(ifconfig "$i" 2>/dev/null | awk '/inet /{for(k=1;k<=NF;k++) if($k=="inet"){a=$(k+1); sub(/^addr:/,"",a); print a}}')"
        else
            ips=""
        fi
        local tipo="Ethernet"
        if [ -d "/sys/class/net/$i/wireless" ] || [ -e "/sys/class/net/$i/phy80211" ]; then tipo="WiFi (802.11)"
        elif [ -d "/sys/devices/virtual/net/$i" ]; then tipo="Virtual"; fi
        W ""
        W "    Adaptador: $i"
        W "      Tipo: $tipo"
        W "      Estado: ${est:-$ND}"
        case "$vel" in ''|-*) W "      Velocidad del enlace: $ND" ;; *) W "      Velocidad del enlace: $vel Mbps" ;; esac
        W "      MAC: ${mac:-$ND}"
        W "      MTU: ${mtu:-$ND}"
        [ -n "$drv" ] && W "      Driver: $drv"
        local ip
        for ip in $ips; do W "      IPv4: $ip"; done
        if [ "$i" = "$IFACE_DEF" ]; then
            W "      Gateway: ${GATEWAY:-$ND} (ruta por defecto${metrica:+, metrica $metrica})"
            MTU_IFACE="$mtu"
        fi
        if [ -n "$ips" ] && es_vpn_nombre "$i"; then
            VPN_ON=1; VPN_LISTA="$VPN_LISTA $i"
            r_obs "INTERFAZ VPN DETECTADA: $i"
        fi
        [ "$tipo" = "WiFi (802.11)" ] && wifi_linux "$i"
    done
    [ "$n" -eq 0 ] && W "    No se detectaron adaptadores activos (ip/sysfs no disponibles)"
    local dns=""
    if tiene resolvectl; then
        dns="$(run_to 10 resolvectl dns 2>/dev/null | awk -F': ' 'NF>1 && $2!="" {print $2}' | tr ' ' '\n' | awk 'NF && !s[$0]++' | tr '\n' ' ')"
    fi
    local rc_dns=""
    [ -r /etc/resolv.conf ] && rc_dns="$(awk '/^nameserver/{print $2}' /etc/resolv.conf | tr '\n' ' ')"
    W ""
    W "    Servidores DNS (resolv.conf): ${rc_dns:-$ND}"
    [ -n "$dns" ] && W "    Servidores DNS (systemd-resolved): $dns"
}

etapa_4() {
    etapa 4 "Red local"
    sub "Adaptadores de red"
    case "$OS" in
        mac) red_mac ;;
        *)   red_linux ;;
    esac
    W ""
    W "    Ruta por defecto: interfaz ${IFACE_DEF:-$ND}, gateway ${GATEWAY:-$ND}"
    if [ -n "$GATEWAY" ]; then r_ok "Gateway predeterminado detectado: $GATEWAY"
    else r_info "No se pudo determinar el gateway predeterminado"; fi
    if [ "$VPN_ON" -eq 1 ]; then
        W "    [ALERTA] Interfaces VPN activas:$VPN_LISTA"
        if [ -n "$IFACE_DEF" ] && es_vpn_nombre "$IFACE_DEF"; then
            r_obs "La ruta por defecto pasa por la VPN ($IFACE_DEF): puede interferir con el routing"
        fi
    fi
}

# =====================================================================
# [5] DNS
# =====================================================================
DNS_TOOL=""
if tiene dig; then DNS_TOOL="dig"; elif tiene host; then DNS_TOOL="host"; elif tiene nslookup; then DNS_TOOL="nslookup"; fi

resolver_sistema() {
    local h="$1" r=""
    if tiene getent; then r="$(run_to 8 getent ahostsv4 "$h" 2>/dev/null | awk '{print $1}' | awk '!s[$0]++')"; fi
    if [ -z "$r" ] && [ "$OS" = "mac" ] && tiene dscacheutil; then
        r="$(run_to 8 dscacheutil -q host -a name "$h" 2>/dev/null | awk '/^ip_address:/{print $2}' | awk '!s[$0]++')"
    fi
    if [ -z "$r" ] && tiene dig; then r="$(run_to 8 dig +short +tries=1 +time=3 "$h" A 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')"; fi
    if [ -z "$r" ] && tiene host; then r="$(run_to 8 host -W 3 -t A "$h" 2>/dev/null | awk '/has address/{print $NF}')"; fi
    if [ -z "$r" ] && tiene nslookup; then
        r="$(run_to 8 nslookup "$h" 2>/dev/null | tr -d '\r' | awk '/^(Name|Nombre):/{f=1; next} f {for(i=1;i<=NF;i++) if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) print $i}')"
    fi
    printf '%s' "$r"
}

# Imprime el tiempo en ms si la consulta tuvo exito; nada si fallo
dns_consulta() {
    local srv="$1" h="$2" t0 t1 out rc
    case "$DNS_TOOL" in
        dig)
            out="$(run_to 5 dig +tries=1 +time=2 "@$srv" "$h" A 2>/dev/null)"
            case "$out" in
                *"status: NOERROR"*) printf '%s\n' "$out" | awk '/Query time:/{print $4; exit}' ;;
            esac ;;
        host)
            t0="$(now_ms)"; out="$(run_to 5 host -W 2 -t A "$h" "$srv" 2>/dev/null)"; rc=$?; t1="$(now_ms)"
            [ "$rc" -eq 0 ] && echo $((t1 - t0)) ;;
        nslookup)
            t0="$(now_ms)"; out="$(run_to 5 nslookup -type=A -timeout=2 -retry=1 "$h" "$srv" 2>/dev/null | tr -d '\r')"; t1="$(now_ms)"
            printf '%s\n' "$out" | grep -Eqi '^(name|nombre):' && echo $((t1 - t0)) ;;
    esac
}

etapa_5() {
    etapa 5 "DNS"
    if es_ipv4 "$API_HOST" || es_loopback "$API_HOST"; then
        DNS_OMITIDO=1
        if es_loopback "$API_HOST"; then API_IPS="127.0.0.1"; else API_IPS="$API_HOST"; fi
        [ "$API_HOST" = "::1" ] && API_IPS="::1"
        API_IP="$API_IPS"
        r_info "Omitiendo DNS: el host de la API es una IP o equipo local ($API_HOST)"
        return
    fi

    sub "Resolucion con el DNS del sistema"
    local t0 t1 ips
    t0="$(now_ms)"; ips="$(resolver_sistema "$API_HOST")"; t1="$(now_ms)"
    if [ -n "$ips" ]; then
        API_IPS="$ips"; API_IP="$(printf '%s\n' "$ips" | head -1)"
        r_ok "DNS sistema resuelto: $API_HOST -> $(printf '%s' "$ips" | tr '\n' ',' | sed 's/,$//') ($((t1 - t0)) ms)"
    else
        r_falla "Error DNS sistema: no se pudo resolver $API_HOST"
    fi

    if [ -z "$DNS_TOOL" ]; then
        r_info "Medicion por servidor DNS no disponible (requiere dig, host o nslookup)"
        return
    fi
    sub "Mediciones por servidor DNS publico (20 por servidor, herramienta: $DNS_TOOL)"
    W "    Esto tomara aproximadamente 1-3 minutos..."
    local linea nombre ip i ms tiempos fallos n prom min max p50 p95 p99 sd tasa
    while IFS='|' read -r nombre ip; do
        [ -z "$ip" ] && continue
        printf '  Probando %s (%s) ' "$nombre" "$ip"
        tiempos=""; fallos=0; i=1
        while [ "$i" -le 20 ]; do
            ms="$(dns_consulta "$ip" "$API_HOST")"
            if [ -n "$ms" ]; then tiempos="${tiempos}${ms}${NL}"; progreso "."
            else fallos=$((fallos + 1)); progreso "x"; fi
            sleep 0.1 2>/dev/null || sleep 1
            i=$((i + 1))
        done
        echo
        read -r n prom min max p50 p95 p99 sd <<EOF
$(printf '%s' "$tiempos" | calc_stats)
EOF
        if [ "$n" -gt 0 ]; then
            tasa="$(awk -v n="$n" 'BEGIN{printf "%.2f", n/20*100}')"
            W ""
            W "    $nombre ($ip):"
            W "      Mediciones exitosas: $n/20 ($tasa%)"
            W "      Tiempo promedio: $prom ms"
            W "      Tiempo minimo: $min ms"
            W "      Tiempo maximo: $max ms"
            W "      Percentil 50 (mediana): $p50 ms"
            W "      Percentil 95: $p95 ms"
            W "      Percentil 99: $p99 ms"
            W "      Desviacion estandar: $sd ms"
            DNS_RES="${DNS_RES}${p95}|${prom}|${tasa}|${nombre}|${ip}${NL}"
            if fgt "$p95" 500; then r_obs "DNS $nombre MUY LENTO: P95 = $p95 ms"; DNS_LENTO=1
            elif fgt "$p95" 200; then r_obs "DNS $nombre lento: P95 = $p95 ms"; DNS_LENTO=1
            elif fgt "$p95" 100; then r_info "DNS $nombre aceptable: P95 = $p95 ms"
            else r_ok "DNS $nombre rapido: P95 = $p95 ms"; fi
        else
            W ""
            W "    $nombre ($ip): todas las consultas fallaron"
            r_falla "DNS $nombre ($ip): Todas las pruebas fallaron"
        fi
    done <<EOF
Google Primary|8.8.8.8
Cloudflare|1.1.1.1
Google Secondary|8.8.4.4
Quad9|9.9.9.9
OpenDNS|208.67.222.222
EOF

    if [ -n "$DNS_RES" ]; then
        W ""
        W "    Ranking de servidores DNS (del mas rapido al mas lento, por P95):"
        local rk=1 est
        while IFS='|' read -r p95 prom tasa nombre ip; do
            [ -z "$nombre" ] && continue
            fgt "$tasa" 50 || continue
            if flt "$p95" 20; then est="[EXCELENTE]"; elif flt "$p95" 50; then est="[BUENO]"; elif flt "$p95" 100; then est="[REGULAR]"; else est="[LENTO]"; fi
            W "      $rk. $nombre - P95: $p95 ms, Prom: $prom ms $est"
            rk=$((rk + 1))
        done <<EOF
$(printf '%s' "$DNS_RES" | sort -t'|' -k1,1n)
EOF
    fi
}

# =====================================================================
# [6] CONECTIVIDAD TCP
# =====================================================================
# Devuelve 0 si el puerto acepta conexiones; imprime el tiempo de conexion (ms) si se pudo medir
tcp_probar() {
    local h="$1" p="$2" t0 t1 r
    t0="$(now_ms)"
    if run_to 5 "$BASH" -c 'exec 3<>"/dev/tcp/$1/$2"' _ "$h" "$p" >/dev/null 2>&1; then
        t1="$(now_ms)"; echo $((t1 - t0)); return 0
    fi
    if tiene nc && run_to 7 nc -z -w 5 "$h" "$p" >/dev/null 2>&1; then
        echo ""; return 0
    fi
    if tiene curl; then
        r="$(curl -s -o /dev/null --noproxy '*' --connect-timeout 5 -m 6 -w '%{time_connect}' "http://$h:$p/" 2>/dev/null)"
        if fgt "${r:-0}" 0; then awk -v s="$r" 'BEGIN{printf "%.0f\n", s*1000}'; return 0; fi
    fi
    return 1
}

etapa_6() {
    etapa 6 "Conectividad TCP"
    local destino="$API_HOST" p ms puertos="$API_PORT"
    [ "$API_PORT" = "443" ] && puertos="443 80"
    for p in $puertos; do
        W "    Probando $destino:$p ..."
        ms="$(tcp_probar "$destino" "$p")"
        if [ $? -eq 0 ]; then
            W "      Puerto $p: ABIERTO${ms:+ (conexion en $ms ms)}"
            [ "$p" = "$API_PORT" ] && TCP_API_OK=1
            if [ "$p" = "$API_PORT" ]; then r_ok "Puerto TCP $p ABIERTO para la API${ms:+ ($ms ms)}"
            else r_ok "Puerto TCP $p ABIERTO"; fi
        else
            W "      Puerto $p: CERRADO/FILTRADO"
            if [ "$p" = "$API_PORT" ]; then
                r_falla "Puerto TCP $p CERRADO o filtrado: el equipo no puede conectarse a la API"
                PUERTO_CERRADO=1; TCP_API_OK=0
            else
                r_info "Puerto TCP $p cerrado o filtrado (no requerido por la API)"
            fi
        fi
    done
}

# =====================================================================
# [7] LATENCIA Y JITTER
# =====================================================================
PING_VAR=""   # variante de ping que funciona: A (rapida) o B (basica)

ping_ejecutar() { # $1 destino $2 cantidad $3 variante
    local n="$2" lim
    case "$3" in
        A)  lim=$(( n / 4 + 8 ))
            if [ "$OS" = "mac" ]; then run_to "$lim" ping -n -c "$n" -i 0.2 -W 1000 "$1" 2>&1
            else run_to "$lim" ping -n -c "$n" -i 0.2 -W 1 "$1" 2>&1; fi ;;
        B)  lim=$(( n * 2 + 8 ))
            run_to "$lim" ping -c "$n" "$1" 2>&1 ;;
    esac
}
ping_valido() { printf '%s\n' "$1" | grep -Eqi 'packets transmitted|transmitted,'; }
ping_tiempos() { printf '%s\n' "$1" | grep 'bytes from' | grep -v 'DUP' | sed -n 's/.*time[=<]\([0-9.]*\).*/\1/p'; }

latencia_tcp() { # $1 cantidad -> tiempos ms (uno por linea), 0 vacio por fallo
    local n="$1" i=1 r
    while [ "$i" -le "$n" ]; do
        if tiene curl; then
            r="$(curl -s -o /dev/null -I "${CURL_TLS[@]}" --noproxy '*' --connect-timeout 3 -m 5 -A "$UA" \
                -w '%{time_namelookup} %{time_connect}' "$API_URL/robots.txt" 2>/dev/null)"
            r="$(printf '%s' "$r" | awk '{ if ($2 > 0) printf "%.2f", ($2 - $1) * 1000 }')"
        else
            r="$(tcp_probar "$API_HOST" "$API_PORT")"
        fi
        if [ -n "$r" ]; then echo "$r"; progreso "." >&2; else echo "FALLO"; progreso "x" >&2; fi
        [ $((i % 25)) -eq 0 ] && echo >&2
        sleep 0.1 2>/dev/null
        i=$((i + 1))
    done
}

evaluar_latencia() { # $1 nombre $2 tiempos $3 fallos $4 metodo $5 es_servidor
    local n prom min max p50 p95 p99 sd perd
    read -r n prom min max p50 p95 p99 sd <<EOF
$(printf '%s\n' "$2" | calc_stats)
EOF
    perd="$(awk -v f="$3" 'BEGIN{printf "%.2f", f/50*100}')"
    W ""
    W "    Estadisticas de latencia para $1 ($n muestras exitosas de 50, metodo: $4):"
    W "      Promedio: $prom ms"
    W "      Minimo: $min ms"
    W "      Maximo: $max ms"
    W "      Percentil 50: $p50 ms"
    W "      Percentil 95: $p95 ms"
    W "      Percentil 99: $p99 ms"
    W "      Jitter (desv. estandar): $sd ms"
    W "      Perdida de paquetes: $perd%"
    if flt "$prom" 50; then r_ok "Latencia EXCELENTE (<50ms) a $1: $prom ms"
    elif flt "$prom" 100; then r_ok "Latencia BUENA (<100ms) a $1: $prom ms"
    elif flt "$prom" 200; then r_obs "Latencia REGULAR (<200ms) a $1: $prom ms"; LAT_ALTA=1
    else r_obs "Latencia ALTA (>200ms) a $1: $prom ms"; LAT_ALTA=1; fi
    if flt "$sd" 5; then r_ok "Jitter EXCELENTE (<5ms) a $1: $sd ms"
    elif flt "$sd" 10; then r_ok "Jitter BUENO (<10ms) a $1: $sd ms"
    elif flt "$sd" 20; then r_obs "Jitter REGULAR (<20ms) a $1: $sd ms"; JIT_ALTO=1
    else r_obs "Jitter ALTO (>20ms) a $1: $sd ms"; JIT_ALTO=1; fi
    if fgt "$perd" 2; then r_obs "Perdida de paquetes de $perd% hacia $1"; PERDIDA=1; fi
    if [ "$5" = "1" ]; then LAT_SRV="$prom"; JIT_SRV="$sd"; PERD_SRV="$perd"; LAT_METODO="$4"
    else LAT_GW="$prom|$perd"; fi
}

etapa_7() {
    etapa 7 "Latencia y jitter"
    local objetivos="" nombre dest o tiempos rec fallos
    [ -n "$GATEWAY" ] && objetivos="Gateway local|$GATEWAY|0${NL}"
    local dsrv="$API_IP"; [ -z "$dsrv" ] && dsrv="$API_HOST"
    objetivos="${objetivos}Servidor API|$dsrv|1"
    if ! tiene ping; then r_info "Comando ping no disponible"; fi
    local es_srv
    while IFS='|' read -r nombre dest es_srv; do
        [ -z "$dest" ] && continue
        W ""
        W "    Destino: $nombre ($dest)"
        printf '  %s (%s): ' "$nombre" "$dest"
        tiempos=""; rec=0
        if tiene ping; then
            # Sondeo corto para elegir variante y detectar bloqueo ICMP
            if [ -z "$PING_VAR" ]; then
                o="$(ping_ejecutar "$dest" 3 A)"
                if ping_valido "$o"; then PING_VAR="A"
                else o="$(ping_ejecutar "$dest" 3 B)"; ping_valido "$o" && PING_VAR="B"; fi
            elif [ -n "$PING_VAR" ]; then
                o="$(ping_ejecutar "$dest" 3 "$PING_VAR")"
            fi
            if [ -n "$PING_VAR" ] && [ -n "$(ping_tiempos "$o")" ]; then
                progreso "(ICMP, 50 pings) "
                o="$(ping_ejecutar "$dest" 50 "$PING_VAR")"
                tiempos="$(ping_tiempos "$o")"
                rec="$(printf '%s\n' "$tiempos" | grep -c '[0-9]')"
            fi
        fi
        if [ "$rec" -gt 0 ]; then
            echo "$rec/50 respuestas"
            fallos=$((50 - rec)); [ "$fallos" -lt 0 ] && fallos=0
            evaluar_latencia "$nombre" "$tiempos" "$fallos" "ICMP (ping)" "$es_srv"
        elif [ "$es_srv" = "1" ]; then
            ICMP_BLOQ=1
            echo "ICMP sin respuesta; usando conexiones TCP"
            W "      ICMP bloqueado o sin respuesta (perdida 100%): se mide con 50 conexiones TCP al puerto $API_PORT"
            r_info "Ping ICMP bloqueado hacia el servidor: latencia medida con conexiones TCP al puerto $API_PORT"
            printf '  '
            o="$(latencia_tcp 50)"
            echo
            tiempos="$(printf '%s\n' "$o" | grep -v FALLO)"
            fallos="$(printf '%s\n' "$o" | grep -c FALLO)"
            rec="$(printf '%s\n' "$tiempos" | grep -c '[0-9]')"
            if [ "$rec" -gt 0 ]; then
                evaluar_latencia "$nombre" "$tiempos" "$fallos" "TCP (conexion al puerto $API_PORT)" 1
            else
                r_falla "No se pudo medir la latencia al servidor (ICMP y TCP fallaron)"
            fi
        else
            echo "sin respuesta ICMP"
            r_info "No se pudieron realizar pings ICMP a $nombre (posiblemente bloqueados)"
        fi
    done <<EOF
$objetivos
EOF
}

# =====================================================================
# [8] MTU
# =====================================================================
mtu_probar() { # $1 destino $2 payload ; 0 = paso sin fragmentar
    local o
    if [ "$OS" = "mac" ]; then o="$(run_to 6 ping -n -c 1 -W 1500 -D -s "$2" "$1" 2>&1)"
    else o="$(run_to 6 ping -n -c 1 -W 2 -M do -s "$2" "$1" 2>&1)"; fi
    MTU_SALIDA="$o"
    printf '%s\n' "$o" | grep -q 'bytes from'
}

etapa_8() {
    etapa 8 "MTU"
    [ -n "$MTU_IFACE" ] && W "    MTU de la interfaz principal ($IFACE_DEF): $MTU_IFACE bytes"
    local dest="$API_IP"; [ -z "$dest" ] && dest="$API_HOST"
    if es_loopback "$dest"; then
        r_info "Prueba de MTU no aplicable: la API esta en este mismo equipo ($dest)"
        return
    fi
    if ! tiene ping; then r_info "Comando ping no disponible: no se puede medir el MTU"; return; fi
    if [ "$ICMP_BLOQ" -eq 1 ] || [ -z "$PING_VAR" ]; then
        r_info "No se puede detectar el MTU de la ruta: ICMP bloqueado o ping no compatible"
        return
    fi
    W "    Probando tamanos con bit 'no fragmentar' hacia $dest ..."
    local mtu ok=0 soporte=1
    for mtu in 1500 1492 1480 1472 1468 1464 1400 1300 1280; do
        if mtu_probar "$dest" $((mtu - 28)); then
            MTU_OPT="$mtu"; ok=1
            W "      $mtu bytes: OK"
            break
        fi
        W "      $mtu bytes: no pasa"
        if printf '%s\n' "$MTU_SALIDA" | grep -Eqi 'invalid|illegal|unrecognized|usage|unknown option'; then soporte=0; break; fi
    done
    if [ "$soporte" -eq 0 ]; then
        r_info "El comando ping de este sistema no permite fijar 'no fragmentar': MTU no medible"
    elif [ "$ok" -eq 1 ]; then
        r_ok "MTU optimo detectado: $MTU_OPT bytes"
        W "    MTU optimo: $MTU_OPT bytes"
        if [ "$MTU_OPT" -lt 1500 ]; then r_obs "MTU menor al estandar (1500): $MTU_OPT. Posible PPPoE o VPN"; fi
    else
        r_obs "No se pudo determinar el MTU optimo"
    fi
}

# =====================================================================
# [9] RUTA DE RED (TRACEROUTE)
# =====================================================================
etapa_9() {
    etapa 9 "Ruta de red (traceroute)"
    local dest="$API_IP"; [ -z "$dest" ] && dest="$API_HOST"
    if es_loopback "$dest" || es_loopback "$API_HOST"; then
        r_info "Omitiendo traceroute: no aplicable para conexiones locales ($dest)"
        W "    Traceroute: No aplicable para conexiones locales"
        r_ok "Traceroute omitido (entorno local)"
        return
    fi
    local herr="" o rc saltos=0 alcanzado=0
    printf '  Ejecutando traceroute (maximo 30 saltos, puede tardar hasta 2 minutos)...\n'
    if tiene traceroute; then
        herr="traceroute"; o="$(run_to 120 traceroute -n -m 30 -w 2 -q 1 "$dest" 2>&1)"; rc=$?
        saltos="$(printf '%s\n' "$o" | awk '/^ *[0-9]+ /{n=$1} END{print n+0}')"
    elif tiene tracepath; then
        herr="tracepath"; o="$(run_to 120 tracepath -n -m 30 "$dest" 2>&1)"; rc=$?
        saltos="$(printf '%s\n' "$o" | sed -n 's/.*hops \([0-9]*\).*/\1/p' | tail -1)"
        [ -z "$saltos" ] && saltos="$(printf '%s\n' "$o" | awk '/^ *[0-9]+[?:]/{n=$1+0} END{print n+0}')"
    elif tiene mtr; then
        herr="mtr"; o="$(run_to 120 mtr -n -r -c 1 -m 30 "$dest" 2>&1)"; rc=$?
        saltos="$(printf '%s\n' "$o" | awk '/^ *[0-9]+\.[|]--/{n=$1+0} END{print n+0}')"
    else
        r_info "Traceroute no disponible (instale traceroute, tracepath o mtr)"
        return
    fi
    W "    Herramienta: $herr"
    W "    Traceroute completo:"
    W_bloque "$o" "      "
    [ "$rc" = "124" ] && r_info "Traceroute interrumpido por tiempo maximo (120 s); resultado parcial"
    printf '%s\n' "$o" | sed '1d' | grep -q "$dest" && alcanzado=1
    W ""
    W "    Analisis de ruta:"
    W "      Total de saltos: $saltos"
    W "      Destino alcanzado: $( [ "$alcanzado" -eq 1 ] && echo Si || echo "No (los ultimos saltos pueden filtrar ICMP/UDP)")"
    if printf '%s\n' "$o" | grep -Eqi 'miami|atlanta|dallas|los angeles|new york|ashburn'; then
        r_obs "Ruta internacional detectada (USA). RTT esperado >100ms"
    fi
    [ "${saltos:-0}" -gt 0 ] && SALTOS="$saltos"
    if [ "${saltos:-0}" -eq 0 ]; then r_info "No se pudo contar los saltos de la ruta"
    elif [ "$saltos" -lt 10 ]; then r_ok "Ruta CORTA (<10 saltos): $saltos"
    elif [ "$saltos" -lt 15 ]; then r_ok "Ruta NORMAL (<15 saltos): $saltos"
    elif [ "$saltos" -lt 20 ]; then r_obs "Ruta LARGA (<20 saltos): $saltos"
    else r_obs "Ruta MUY LARGA (>20 saltos): $saltos"; fi
}

# =====================================================================
# [10] HTTP/HTTPS
# =====================================================================
CURL_TLS=()
CURL_FMT='%{http_code}|%{http_version}|%{time_namelookup}|%{time_connect}|%{time_appconnect}|%{time_starttransfer}|%{time_total}|%{size_download}|%{remote_ip}'

# http_llamar <ruta> <sufijo> [args extra de curl]: imprime el formato CURL_FMT; rc = codigo de curl
http_llamar() {
    local ruta="$1" suf="$2"; shift 2
    curl -sS "${CURL_TLS[@]}" -A "$UA" -H 'Accept: application/json' --connect-timeout 10 -m 20 \
        -o "$TMPD/body_$suf" -D "$TMPD/hdr_$suf" -w "$CURL_FMT" "$@" "$API_URL$ruta" 2>"$TMPD/err_$suf"
}

json_str()  { sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$1" 2>/dev/null | head -1; }
json_num()  { sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\(-\{0,1\}[0-9][0-9.]*\).*/\1/p" "$1" 2>/dev/null | head -1; }
json_bool() { sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*(true|false).*/\1/p" "$1" 2>/dev/null | head -1; }

retry_after() { grep -i '^retry-after:' "$1" 2>/dev/null | tail -1 | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r'; }

fecha_epoch() {
    date -d "$1" +%s 2>/dev/null && return
    date -j -f '%b %d %T %Y %Z' "$1" +%s 2>/dev/null
}

http_certificado() {
    local h="$API_HOST" p="$API_PORT" pem="$TMPD/cert.pem" fin emisor sujeto ep ahora dias
    if tiene openssl; then
        printf '' | run_to 12 openssl s_client -connect "$h:$p" -servername "$h" 2>/dev/null \
            | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' > "$pem" 2>/dev/null
    fi
    if [ -s "$pem" ]; then
        sujeto="$(openssl x509 -in "$pem" -noout -subject 2>/dev/null | sed 's/^subject= *//')"
        emisor="$(openssl x509 -in "$pem" -noout -issuer 2>/dev/null | sed 's/^issuer= *//')"
        fin="$(openssl x509 -in "$pem" -noout -enddate 2>/dev/null | sed 's/^notAfter=//')"
        W "      Sujeto: ${sujeto:-$ND}"
        W "      Emisor: ${emisor:-$ND}"
        W "      Valido desde: $(openssl x509 -in "$pem" -noout -startdate 2>/dev/null | sed 's/^notBefore=//')"
        W "      Valido hasta: ${fin:-$ND}"
        W "      Huella SHA-256: $(openssl x509 -in "$pem" -noout -fingerprint -sha256 2>/dev/null | sed 's/^.*=//')"
    else
        local v="$TMPD/err_v"
        sujeto="$(sed -n 's/^\*[[:space:]]*subject: //p' "$v" 2>/dev/null | head -1)"
        emisor="$(sed -n 's/^\*[[:space:]]*issuer: //p' "$v" 2>/dev/null | head -1)"
        fin="$(sed -n 's/^\*[[:space:]]*expire date: //p' "$v" 2>/dev/null | head -1)"
        W "      Sujeto: ${sujeto:-$ND}"
        W "      Emisor: ${emisor:-$ND}"
        W "      Valido hasta: ${fin:-$ND}"
    fi
    if [ "$ES_DESARROLLO" -eq 1 ] || es_loopback "$h"; then
        r_info "Certificado de desarrollo: no se evalua su vigencia"
        return
    fi
    if [ -n "$fin" ]; then
        ep="$(fecha_epoch "$fin")"; ahora="$(date +%s)"
        if [ -n "$ep" ]; then
            dias=$(( (ep - ahora) / 86400 )); CERT_DIAS="$dias"
            if [ "$dias" -lt 0 ]; then r_falla "Certificado EXPIRADO"; CERT_MAL=1
            elif [ "$dias" -lt 30 ]; then r_obs "Certificado expira en $dias dias"
            else r_ok "Certificado valido por $dias dias"; fi
            return
        fi
        if [ -s "$pem" ]; then
            if ! openssl x509 -in "$pem" -noout -checkend 0 >/dev/null 2>&1; then r_falla "Certificado EXPIRADO"; CERT_MAL=1
            elif ! openssl x509 -in "$pem" -noout -checkend 2592000 >/dev/null 2>&1; then r_obs "Certificado expira en menos de 30 dias"
            else r_ok "Certificado vigente por mas de 30 dias"; fi
            return
        fi
    fi
    r_info "No se pudo leer la fecha de vencimiento del certificado"
}

etapa_10() {
    etapa 10 "HTTP/HTTPS"
    if ! tiene curl; then
        r_falla "curl no esta instalado: no se pueden ejecutar las pruebas HTTP/HTTPS"
        return
    fi
    local cv h2=0 res rc code ver tdns tcon tapp tttfb ttot size rip
    cv="$(curl -V 2>/dev/null | head -1)"
    curl -V 2>/dev/null | grep -qi 'HTTP2' && h2=1
    W "    Cliente: $cv"
    W "    Soporte HTTP/2 en curl: $( [ "$h2" -eq 1 ] && echo Si || echo No )"
    [ -n "${https_proxy}${HTTPS_PROXY}${http_proxy}${HTTP_PROXY}${ALL_PROXY}" ] && W "    Nota: curl usa el proxy definido en variables de entorno"
    [ "$ES_DESARROLLO" -eq 1 ] && [ "$API_SCHEME" = "https" ] && CURL_TLS=(-k)

    local extra=()
    [ "$API_SCHEME" = "https" ] && [ "$h2" -eq 1 ] && extra=(--http2)

    # --- Solicitud 1: capacidades (verbose) ---
    sub "Capacidades del servidor (GET /api/v1/test)"
    local ta tb
    ta="$(now_ms)"
    res="$(http_llamar /api/v1/test v "${extra[@]}" -v)"; rc=$?
    tb="$(now_ms)"
    if [ "$API_SCHEME" = "https" ] && [ "${#CURL_TLS[@]}" -eq 0 ]; then
        case "$rc" in
            35|51|58|60|77|83)
                local em; em="$(grep -i '^curl:' "$TMPD/err_v" | head -1)"
                W "    Error de verificacion TLS: ${em:-codigo $rc}"
                if es_loopback "$API_HOST"; then r_info "Certificado no verificable (esperado en equipo local)"
                else r_falla "Certificado TLS no valido o no confiable para $API_HOST (curl $rc)"; CERT_MAL=1; fi
                CURL_TLS=(-k)
                ta="$(now_ms)"
                res="$(http_llamar /api/v1/test v "${extra[@]}" -v)"; rc=$?
                tb="$(now_ms)" ;;
        esac
    fi
    IFS='|' read -r code ver tdns tcon tapp tttfb ttot size rip <<EOF
$res
EOF
    if [ "$rc" -ne 0 ] || [ "${code:-000}" = "000" ]; then
        local em; em="$(grep -i '^curl:' "$TMPD/err_v" | head -1)"
        W "    Sin respuesta: ${em:-codigo curl $rc}"
        r_falla "No se pudo conectar con la API ($API_URL): ${em:-curl $rc}"
        API_CAIDA=1
        return
    fi
    HTTP_VER="$ver"
    W "    Codigo HTTP: $code"
    W "    Version HTTP negociada: HTTP/$ver"
    W "    IP remota: ${rip:-$ND}"
    local srvh; srvh="$(grep -i '^server:' "$TMPD/hdr_v" 2>/dev/null | tail -1 | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r')"
    W "    Servidor: ${srvh:-No informado}"
    if [ "$API_SCHEME" = "https" ]; then
        local tlsv alpn
        tlsv="$(sed -n 's/.*SSL connection using \([^ ]*\).*/\1/p' "$TMPD/err_v" | head -1)"
        [ -z "$tlsv" ] && tlsv="$(grep -Eo 'TLSv1(\.[0-3])?' "$TMPD/err_v" | tail -1)"
        alpn="$(grep -Ei 'ALPN.*accepted' "$TMPD/err_v" | head -1 | sed 's/^\* *//')"
        W "    Protocolo SSL/TLS negociado: ${tlsv:-$ND}"
        [ -n "$alpn" ] && W "    ALPN: $alpn"
        case "$tlsv" in
            TLSv1.3|TLSv1.2|TLS1.3|TLS1.2) r_ok "Protocolo TLS moderno: $tlsv" ;;
            TLSv1|TLSv1.0|TLSv1.1|TLS1.0|TLS1.1) r_obs "Protocolo TLS obsoleto negociado: $tlsv" ;;
            "") r_info "No se pudo determinar la version TLS (depende de la biblioteca TLS de curl)" ;;
            *) r_info "Version TLS negociada: $tlsv" ;;
        esac
        case "$ver" in
            2|3) r_ok "HTTP/$ver soportado" ;;
            *) if [ "$h2" -eq 1 ]; then
                   r_obs "Servidor usando HTTP/$ver. HTTP/2 mejoraria el rendimiento"; HTTP11=1
               else
                   r_info "HTTP/$ver negociado (curl local sin soporte HTTP/2: no se puede verificar HTTP/2)"
               fi ;;
        esac
        W ""
        W "    Certificado SSL/TLS:"
        http_certificado
    else
        if [ "$ES_DESARROLLO" -eq 1 ] || es_loopback "$API_HOST"; then
            r_info "Conexion HTTP sin TLS (aceptable en desarrollo); HTTP/2 no aplica"
        else
            r_obs "La API se accede por HTTP sin cifrar (sin TLS)"
        fi
    fi

    # --- Mediciones de tiempo (1 + 3 solicitudes a /api/v1/test) ---
    sub "Tiempos de respuesta (GET /api/v1/test, 4 mediciones)"
    local totales="" i=1 med="$res" codm sufm="v"
    while [ "$i" -le 4 ]; do
        IFS='|' read -r codm ver tdns tcon tapp tttfb ttot size rip <<EOF
$med
EOF
        if [ "$codm" = "429" ]; then
            local ra; ra="$(retry_after "$TMPD/hdr_$sufm")"
            W "    Medicion $i: HTTP 429 (limite de uso alcanzado)"
            r_obs "Limite de uso de la API alcanzado (HTTP 429)${ra:+; Retry-After: $ra s}. Se omiten llamadas restantes"
            LIMITE_429=1
            break
        fi
        awk -v n="$i" -v c="$codm" -v v="$ver" -v a="$tdns" -v b="$tcon" -v t="$tapp" -v f="$tttfb" -v z="$ttot" -v s="$size" 'BEGIN{
            tcp = (b - a) * 1000; tls = (t > 0) ? (t - b) * 1000 : 0; base = (t > 0) ? t : b
            printf("    Medicion %d: HTTP %s (HTTP/%s) | DNS %.1f ms | TCP %.1f ms | TLS %.1f ms | espera servidor %.1f ms | primer byte %.1f ms | total %.1f ms | %d bytes\n",
                   n, c, v, a * 1000, tcp, tls, (f - base) * 1000, f * 1000, z * 1000, s)
        }' >> "$OUT"
        [ "$codm" = "200" ] && totales="${totales}$(awk -v z="$ttot" 'BEGIN{printf "%.2f", z*1000}')${NL}"
        i=$((i + 1))
        [ "$i" -gt 4 ] && break
        sleep 0.5 2>/dev/null || sleep 1
        sufm="$i"
        med="$(http_llamar /api/v1/test "$i" "${extra[@]}")"
    done
    if [ -n "$totales" ]; then
        local n prom min max p50 p95 p99 sd
        read -r n prom min max p50 p95 p99 sd <<EOF
$(printf '%s' "$totales" | calc_stats)
EOF
        HTTP_TOT="$prom"; P95_HTTP="$p95"
        W "    Resumen: promedio $prom ms, minimo $min ms, maximo $max ms, P95 $p95 ms ($n mediciones)"
        if flt "$p95" 500; then r_ok "Tiempo de respuesta HTTP EXCELENTE (P95 <500ms): $p95 ms"
        elif flt "$p95" 1000; then r_ok "Tiempo de respuesta HTTP BUENO (P95 <1s): $p95 ms"
        elif flt "$p95" 2000; then r_obs "Tiempo de respuesta HTTP REGULAR (P95 <2s): $p95 ms"
        else r_obs "Tiempo de respuesta HTTP LENTO (P95 >2s): $p95 ms"; fi
    elif [ "$LIMITE_429" -eq 0 ]; then
        r_obs "GET /api/v1/test no respondio 200 (codigo $code)"
    fi

    # --- Contenido de /api/v1/test ---
    local b="$TMPD/body_v" st sms
    if [ "$code" = "200" ] && [ -s "$b" ]; then
        st="$(json_str "$b" status)"; sms="$(json_num "$b" server_time_ms)"
        W ""
        W "    Respuesta de /api/v1/test: status=${st:-$ND}, timestamp=$(json_str "$b" timestamp)"
        if [ "$st" = "ok" ]; then r_ok "Endpoint /api/v1/test responde status=ok"
        else r_obs "Endpoint /api/v1/test respondio un estado inesperado (${st:-sin status})"; fi
        if [ -n "$sms" ]; then
            local desf abs
            desf=$(( ${sms%%.*} - (ta + tb) / 2 ))
            abs=${desf#-}
            if [ "$RELOJ" = "s" ]; then W "    Diferencia de reloj aprox. con el servidor: $desf ms (precision de 1 s)"
            else W "    Diferencia de reloj aprox. con el servidor: $desf ms"; fi
            if [ "$abs" -gt 60000 ]; then
                r_obs "Reloj del equipo desfasado ~$((abs / 1000)) s respecto del servidor (afecta TLS y sesiones)"; RELOJ_DESF=1
            else
                r_ok "Reloj del equipo sincronizado con el servidor (diferencia < 60 s)"
            fi
        fi
    fi

    # --- Solicitud 5: /api/v1/test/health ---
    if [ "$LIMITE_429" -eq 1 ]; then
        r_info "Se omite /api/v1/test/health por limite de uso"
        return
    fi
    sub "Estado del servicio (GET /api/v1/test/health)"
    sleep 0.5 2>/dev/null
    res="$(http_llamar /api/v1/test/health h "${extra[@]}")"; rc=$?
    IFS='|' read -r code ver tdns tcon tapp tttfb ttot size rip <<EOF
$res
EOF
    W "    Codigo HTTP: ${code:-000}   Tiempo total: $(awk -v z="${ttot:-0}" 'BEGIN{printf "%.1f", z*1000}') ms"
    local hb="$TMPD/body_h" sano hst
    case "$code" in
        200)
            sano="$(json_bool "$hb" healthy)"; hst="$(json_str "$hb" status)"
            W "    healthy=${sano:-$ND}, status=${hst:-$ND}, timestamp=$(json_str "$hb" timestamp)"
            if [ "$sano" = "true" ] || [ "$hst" = "healthy" ]; then r_ok "Servicio en estado SALUDABLE"
            else r_obs "Servicio reporta estado NO SALUDABLE"; fi ;;
        429)
            local ra; ra="$(retry_after "$TMPD/hdr_h")"
            r_obs "Limite de uso alcanzado en /api/v1/test/health (HTTP 429)${ra:+; Retry-After: $ra s}"; LIMITE_429=1 ;;
        503)
            r_falla "Servicio NO DISPONIBLE (503 Service Unavailable)"; API_CAIDA=1 ;;
        000|"")
            r_falla "No se pudo acceder a /api/v1/test/health (sin respuesta)" ;;
        *)
            r_obs "Respuesta inesperada de /api/v1/test/health: HTTP $code" ;;
    esac
}

# =====================================================================
# [11] PROXY Y FIREWALL
# =====================================================================
ocultar_cred() { printf '%s' "$1" | sed -E 's#(://)[^/@]*@#\1***@#'; }

etapa_11() {
    etapa 11 "Proxy y firewall"
    sub "Variables de entorno de proxy"
    local v val hay=0
    for v in http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy no_proxy NO_PROXY; do
        eval "val=\${$v}"
        if [ -n "$val" ]; then
            W "    $v=$(ocultar_cred "$val")"
            case "$v" in no_proxy|NO_PROXY) ;; *) hay=1 ;; esac
        fi
    done
    [ "$hay" -eq 0 ] && W "    Sin variables de proxy definidas"

    if [ "$OS" = "mac" ]; then
        sub "Proxy del sistema (scutil --proxy)"
        local px
        px="$(run_to 10 scutil --proxy 2>/dev/null)"
        if [ -n "$px" ]; then
            printf '%s\n' "$px" | awk -F' : ' '
                { k = $1; gsub(/^[ \t]+|[ \t]+$/, "", k); v = $2 }
                k ~ /^(HTTPEnable|HTTPProxy|HTTPPort|HTTPSEnable|HTTPSProxy|HTTPSPort|SOCKSEnable|SOCKSProxy|ProxyAutoConfigEnable|ProxyAutoConfigURLString|ProxyAutoDiscoveryEnable)$/ { print "    " k ": " v }' >> "$OUT"
            if printf '%s\n' "$px" | grep -Eq '(HTTPEnable|HTTPSEnable|SOCKSEnable|ProxyAutoConfigEnable) : 1'; then hay=1; fi
        else
            W "    $ND"
        fi
        sub "Firewall de aplicaciones de macOS"
        local fw="/usr/libexec/ApplicationFirewall/socketfilterfw"
        if [ -x "$fw" ]; then
            W "    $(run_to 10 "$fw" --getglobalstate 2>/dev/null)"
            W "    $(run_to 10 "$fw" --getblockall 2>/dev/null)"
            W "    $(run_to 10 "$fw" --getstealthmode 2>/dev/null)"
        else
            W "    $ND"
        fi
        W "    Filtro de paquetes (pf): requiere privilegios de administrador para consultarse"
    elif [ "$OS" = "linux" ]; then
        sub "Proxy del escritorio"
        local modo=""
        if tiene gsettings; then
            modo="$(run_to 5 gsettings get org.gnome.system.proxy mode 2>/dev/null | tr -d "'")"
            if [ -n "$modo" ]; then
                W "    GNOME: modo=$modo"
                case "$modo" in
                    manual) hay=1
                        W "      HTTPS: $(run_to 5 gsettings get org.gnome.system.proxy.https host 2>/dev/null):$(run_to 5 gsettings get org.gnome.system.proxy.https port 2>/dev/null)"
                        W "      HTTP: $(run_to 5 gsettings get org.gnome.system.proxy.http host 2>/dev/null):$(run_to 5 gsettings get org.gnome.system.proxy.http port 2>/dev/null)" ;;
                    auto) hay=1
                        W "      PAC: $(run_to 5 gsettings get org.gnome.system.proxy autoconfig-url 2>/dev/null)" ;;
                esac
            fi
        fi
        if [ -r "$HOME/.config/kioslaverc" ]; then
            local kt; kt="$(sed -n 's/^ProxyType=//p' "$HOME/.config/kioslaverc" | head -1)"
            if [ -n "$kt" ]; then W "    KDE: ProxyType=$kt"; [ "$kt" != "0" ] && hay=1; fi
        fi
        if [ -r /etc/environment ] && grep -qi 'proxy' /etc/environment 2>/dev/null; then
            W "    /etc/environment define variables de proxy:"
            W_bloque "$(grep -i 'proxy' /etc/environment | while IFS= read -r l; do ocultar_cred "$l"; echo; done)" "      "
        fi
        [ -z "$modo" ] && [ ! -r "$HOME/.config/kioslaverc" ] && W "    Configuracion de proxy de escritorio: $ND"

        sub "Firewall"
        local alguno=0 st
        if tiene ufw; then
            st="$(run_to 5 ufw status 2>&1 | head -1)"
            case "$st" in *root*|*permission*|*Permission*) st="requiere privilegios" ;; esac
            W "    ufw: $st"; alguno=1
        fi
        if tiene firewall-cmd; then
            W "    firewalld: $(run_to 8 firewall-cmd --state 2>&1 | head -1)"; alguno=1
        fi
        if tiene systemctl; then
            local u
            for u in ufw firewalld nftables iptables netfilter-persistent; do
                st="$(systemctl is-active "$u" 2>/dev/null)"
                case "$st" in active|inactive|failed) W "    Servicio $u: $st"; alguno=1 ;; esac
            done
        fi
        [ "$alguno" -eq 0 ] && W "    $ND (la consulta de iptables/nftables requiere privilegios)"
    else
        W ""
        W "    Proxy y firewall del sistema: $ND en esta plataforma"
    fi

    if [ "$hay" -eq 1 ]; then r_obs "Proxy ACTIVO - puede afectar conectividad"; PROXY_ON=1
    else r_ok "Sin proxy configurado"; fi
    r_info "Estado del firewall registrado en el informe (solo informativo)"
}

# =====================================================================
# [12] RESUMEN EJECUTIVO
# =====================================================================
etapa_12() {
    etapa 12 "Resumen ejecutivo"
    # Las cifras se toman ANTES de escribir el resumen para no alterarlas
    local ok=$N_OK obs=$N_OBS fal=$N_FALLA

    W "  Equipo:"
    W "    Sistema: $PLATAFORMA"
    W "    CPU: ${CPU_MODELO:-$ND}${CPU_NUCLEOS:+ ($CPU_NUCLEOS núcleos)}"
    [ -n "$RAM_TOTAL_GB" ] && W "    RAM: $RAM_TOTAL_GB GB total, $RAM_DISP_GB GB disponible"
    [ -n "$GPU_PRINCIPAL" ] && W "    GPU: $GPU_PRINCIPAL"
    [ "$DISCOS_TOTAL" -gt 0 ] && W "    Almacenamiento: $DISCOS_TOTAL discos ($DISCOS_SSD SSD, $DISCOS_HDD HDD)"
    [ -n "$UPTIME_DIAS" ] && W "    Tiempo activo: $UPTIME_DIAS días"
    W "  Red:"
    W "    Puerta de enlace: ${GATEWAY:-no detectada}"
    [ -n "$WIFI_PCT" ] && W "    Señal WiFi: $WIFI_PCT%"
    if [ "$VPN_ON" -eq 1 ]; then W "    VPN activa: Sí (${VPN_LISTA# })"; else W "    VPN activa: No"; fi
    if [ "$PROXY_ON" -eq 1 ]; then W "    Proxy: Activo"; else W "    Proxy: No"; fi
    local mejor="" mejor_nom="" mejor_ip="" mejor_p95=""
    if [ -n "$DNS_RES" ]; then
        mejor="$(printf '%s' "$DNS_RES" | sort -t'|' -k1,1n | head -1)"
        mejor_p95="$(printf '%s' "$mejor" | cut -d'|' -f1)"
        mejor_nom="$(printf '%s' "$mejor" | cut -d'|' -f4)"
        mejor_ip="$(printf '%s' "$mejor" | cut -d'|' -f5)"
        W "    Mejor DNS público: $mejor_nom (P95 $mejor_p95 ms)"
    elif [ "$DNS_OMITIDO" -eq 1 ]; then
        W "    DNS: omitido (host local o IP)"
    fi
    W "  API Authentic:"
    case "$TCP_API_OK" in
        1) W "    Puerto TCP $API_PORT: abierto" ;;
        0) W "    Puerto TCP $API_PORT: cerrado o filtrado" ;;
    esac
    [ -n "$LAT_GW" ] && W "    Latencia a la puerta de enlace: ${LAT_GW%%|*} ms (pérdida ${LAT_GW##*|}%)"
    [ -n "$LAT_SRV" ] && W "    Latencia al servidor: $LAT_SRV ms (jitter $JIT_SRV ms, pérdida $PERD_SRV%) por $LAT_METODO"
    [ -n "$MTU_OPT" ] && W "    MTU de ruta: $MTU_OPT bytes"
    [ -n "$SALTOS" ] && W "    Saltos de red: $SALTOS"
    [ -n "$HTTP_VER" ] && W "    Versión HTTP: HTTP/$HTTP_VER"
    [ -n "$CERT_DIAS" ] && W "    Certificado: vence en $CERT_DIAS días"
    [ -n "$P95_HTTP" ] && W "    Tiempo de respuesta /api/v1/test (P95): $P95_HTTP ms"

    if [ -n "$L_FALLA" ]; then
        W ""
        W "  Problemas detectados:"
        printf '%s' "$L_FALLA" | sed 's/^/    [FALLA] /' >> "$OUT"
    fi
    if [ -n "$L_OBS" ]; then
        W ""
        W "  Observaciones:"
        printf '%s' "$L_OBS" | sed 's/^/    [OBS] /' >> "$OUT"
    fi

    W ""
    W "  Resultados: OK=$ok  OBS=$obs  FALLA=$fal"
    local veredicto color
    if [ "$fal" -gt 0 ]; then veredicto="Se detectaron problemas de conectividad"; color="$C_ROJO"
    elif [ "$obs" -gt 0 ]; then veredicto="Conectividad funcional con observaciones"; color="$C_AMAR"
    else veredicto="Conectividad óptima"; color="$C_VERDE"; fi
    W "  Veredicto: $veredicto"
    printf '\n  Resultados: OK=%s  OBS=%s  FALLA=%s\n' "$ok" "$obs" "$fal"
    printf '%b  Veredicto: %s%b\n' "$color" "$veredicto" "$C_RST"

    # Recomendaciones priorizadas: "prioridad|tipo|impacto|accion|beneficio"
    local recs="" sug=""
    [ -n "$mejor_nom" ] && sug=" (el más rápido fue $mejor_nom, $mejor_ip)"
    [ "$TCP_API_OK" = "0" ] && recs="${recs}1|CONEXION|ALTO|Permitir la salida al puerto TCP $API_PORT hacia $API_HOST en el firewall, antivirus o router|Sin este puerto la aplicación no puede comunicarse con la API${NL}"
    [ "$API_CAIDA" -eq 1 ] && recs="${recs}1|API|ALTO|Verificar la conexión a Internet y contactar a soporte de 2Tec Sistemas si persiste|La API no respondió correctamente desde este equipo${NL}"
    if printf '%s' "$L_FALLA" | grep -q 'DNS sistema'; then
        recs="${recs}1|DNS|ALTO|Revisar el DNS configurado en el equipo o router; probar con 1.1.1.1 u 8.8.8.8|Sin resolución DNS la aplicación no encuentra la API${NL}"
    elif [ "$DNS_LENTO" -eq 1 ]; then
        recs="${recs}2|DNS|ALTO|Configurar el DNS con los servidores más rápidos detectados${sug}|Reduce la latencia inicial de cada conexión${NL}"
    fi
    [ "$CERT_MAL" -eq 1 ] && recs="${recs}2|TLS|ALTO|Revisar fecha y hora del equipo, antivirus con inspección HTTPS o proxy corporativo|El certificado del servidor no pudo validarse${NL}"
    [ "$VPN_ON" -eq 1 ] && recs="${recs}2|VPN|ALTO|Deshabilitar las interfaces VPN durante la operación normal (${VPN_LISTA# })|Elimina rutas conflictivas y reduce la latencia${NL}"
    [ "$PROXY_ON" -eq 1 ] && recs="${recs}2|PROXY|ALTO|Agregar $API_HOST a las excepciones del proxy o verificar que el proxy permita la conexión|Evita bloqueos e inspección intermedia del tráfico${NL}"
    if [ -n "$PERD_SRV" ] && fgt "$PERD_SRV" 0; then
        recs="${recs}2|RED|ALTO|Revisar cableado, router o proveedor de Internet: hay $PERD_SRV% de pérdida hacia la API|La pérdida de paquetes provoca reintentos y lentitud${NL}"
    elif [ "$LAT_ALTA" -eq 1 ] || [ "$JIT_ALTO" -eq 1 ]; then
        recs="${recs}3|RED|MEDIO|Revisar la saturación de la red local o consultar con el proveedor de Internet|Latencia o jitter elevados afectan la fluidez de la aplicación${NL}"
    fi
    [ "$WIFI_DEBIL" -eq 1 ] && recs="${recs}3|WIFI|MEDIO|Acercar el equipo al router o conectarlo por cable de red|Una señal débil genera latencia variable y cortes${NL}"
    [ "$MEM_BAJA" -eq 1 ] && recs="${recs}3|MEMORIA|MEDIO|Liberar memoria o agregar más RAM (actualmente $RAM_DISP_GB GB disponibles)|Evita micro-pausas por uso de memoria virtual${NL}"
    [ "$RELOJ_DESF" -eq 1 ] && recs="${recs}3|RELOJ|MEDIO|Sincronizar la fecha y hora del equipo (hora automática / NTP)|Evita errores de sesión y de certificados${NL}"
    if [ -n "$MTU_OPT" ] && [ "$MTU_OPT" -lt 1500 ] 2>/dev/null; then
        recs="${recs}3|MTU|MEDIO|Revisar el MTU del router (PPPoE/VPN): la ruta admite $MTU_OPT bytes|Evita fragmentación y cortes en transferencias grandes${NL}"
    fi
    [ "$DISCO_LLENO" -eq 1 ] && recs="${recs}3|DISCO|BAJO|Liberar espacio en los volúmenes con menos de 10% libre|Evita lentitud del sistema y fallas al guardar archivos${NL}"
    [ "$TEMP_ALTA" -eq 1 ] && recs="${recs}3|TEMPERATURA|BAJO|Revisar ventilación y limpieza del equipo|Evita la reducción de rendimiento por temperatura${NL}"
    [ "$UPTIME_LARGO" -eq 1 ] && recs="${recs}3|SISTEMA|BAJO|Reiniciar el equipo|Libera recursos y aplica actualizaciones pendientes${NL}"
    [ "$HTTP11" -eq 1 ] && recs="${recs}4|PROTOCOLO|MEDIO|Solicitar la habilitación de HTTP/2 en el servidor (a 2Tec Sistemas)|Reduce la latencia con múltiples solicitudes simultáneas${NL}"
    [ "$LIMITE_429" -eq 1 ] && recs="${recs}4|LIMITE|BAJO|Esperar un minuto antes de repetir el diagnóstico|La API aplicó su límite de uso (HTTP 429)${NL}"

    W ""
    W "  Recomendaciones:"
    if [ -z "$recs" ]; then
        W "    Sin recomendaciones: no se detectaron acciones necesarias en este equipo."
    else
        local k=1 pr ti im ac be
        while IFS='|' read -r pr ti im ac be; do
            [ -z "$ti" ] && continue
            W "    $k. [$ti] Impacto: $im"
            W "       Acción: $ac"
            W "       Beneficio: $be"
            k=$((k + 1))
        done <<EOF
$(printf '%s' "$recs" | sort -t'|' -s -k1,1n)
EOF
    fi
    W ""
    W "  Las pruebas de la API (endpoints, latencia de aplicación y velocidad) se ejecutan desde el panel web de Authentic."
    W ""
    W "================================================================"
    W " Fin del diagnóstico - $(date '+%Y-%m-%d %H:%M:%S')"
    W " Este informe se genera y queda solo en este equipo."
    W "================================================================"
}

# =====================================================================
# EJECUCION
# =====================================================================
etapa_1
etapa_2
etapa_3
etapa_4
etapa_5
etapa_6
etapa_7
etapa_8
etapa_9
etapa_10
etapa_11
etapa_12

printf '\n%b================================================================%b\n' "$C_MAG" "$C_RST"
printf '%b  INFORME GENERADO%b\n' "$C_MAG" "$C_RST"
printf '%b================================================================%b\n' "$C_MAG" "$C_RST"
printf '%b  Archivo:%b\n' "$C_CIAN" "$C_RST"
printf '%b  %s%b\n' "$C_AMAR" "$OUT" "$C_RST"
echo "  El informe queda solo en este equipo. Compártalo con soporte si se lo solicitan."
echo "  Las pruebas de la API (endpoints, latencia de aplicación y velocidad) se ejecutan desde el panel web de Authentic."

if [ "$N_FALLA" -gt 0 ]; then exit 1; fi
exit 0
