#!/usr/bin/env bash
# Yggdrasil public-peers reachability + latency check, run directly from a server's
# own terminal instead of a browser. Useful because a browser-based check reflects
# whatever WiFi/VPN path the visitor happens to be on, which adds its own latency
# and jitter unrelated to the peer itself -- running this on (or close to) the
# machine you actually care about gives more representative numbers.
#
# Same duplicate check and config suggestion as ygg-peer-check.html: results are
# deduplicated by resolved IP (two different hostnames pointing at the same box
# only count once) and, at the end, the 4 fastest distinct servers are printed as
# a ready-to-paste yggdrasil.conf "Peers" snippet, ranked by 7-day uptime from the
# shared store first and this run's latency as a tiebreaker.
#
# Usage:
#   ./ygg-peer-check.sh                 # print results + suggestion only
#   YGG_REPORT_URL=https://bruijn.nu/ygg-uptime.php ./ygg-peer-check.sh
#                                        # also feed this run's results into the shared uptime store
#
# Requires: bash, curl or wget, getent (present on virtually every Linux install).
# python3 is used for the actual TCP connect check when available (see check_tcp
# below), and opportunistically to read 7-day uptime stats for ranking the
# suggestion; without it, both degrade gracefully (bash's /dev/tcp as a TCP
# fallback, latency-only ranking for the suggestion).
# Note: quic:// peers use UDP, so a TCP connect check will always show them as
# unreachable here -- same blind spot as the browser-based and cron checks.

set -uo pipefail

TREE_URL="https://api.github.com/repos/yggdrasil-network/public-peers/git/trees/master?recursive=1"
RAW_BASE="https://raw.githubusercontent.com/yggdrasil-network/public-peers/master"
TIMEOUT_SECONDS=2
CONCURRENCY=25
REPORT_URL="${YGG_REPORT_URL:-}"
STATS_URL="${YGG_STATS_URL:-https://bruijn.nu/ygg-uptime.php?action=stats}"

fetch() {
    if command -v curl >/dev/null 2>&1; then
        curl -sS --max-time 20 "$1"
    elif command -v wget >/dev/null 2>&1; then
        wget -qO- --timeout=20 "$1"
    else
        echo "Need curl or wget installed." >&2
        exit 1
    fi
}

# Literal IP -> itself; hostname -> first address getent resolves. Empty on failure.
resolve_ip() {
    local host="$1"
    if [[ "$host" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || [[ "$host" == *:* ]]; then
        echo "$host"
        return
    fi
    getent hosts "$host" 2>/dev/null | awk '{print $1; exit}'
}

HAS_PYTHON3=0
command -v python3 >/dev/null 2>&1 && HAS_PYTHON3=1

# Real TCP connect check. Prefers python3's socket module, which has correct,
# portable IPv4/IPv6 support. Falls back to bash's own /dev/tcp when python3
# isn't available -- but that has a known, real limitation: on some bash builds
# /dev/tcp simply does not support IPv6 at all (confirmed: it can fail to reach
# an always-up IPv6 host like Cloudflare's 2606:4700:4700::1111 even when ping6
# and everything else works fine), which would misreport working IPv6 peers as
# unreachable. So IPv6 results without python3 should be taken with a grain of salt.
check_tcp() {
    local host="$1" port="$2" timeout="$3"
    if [ "$HAS_PYTHON3" -eq 1 ]; then
        python3 -c "
import socket, sys
host, port, timeout = sys.argv[1], int(sys.argv[2]), float(sys.argv[3])
try:
    socket.create_connection((host, port), timeout=timeout).close()
    sys.exit(0)
except Exception:
    sys.exit(1)
" "$host" "$port" "$timeout" 2>/dev/null
    else
        timeout "$timeout" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null
    fi
}

check_one() {
    local peer="$1" location="$2" host="$3" port="$4" out="$5"
    local ip start end latency
    ip=$(resolve_ip "$host")

    if [ -z "$ip" ]; then
        printf 'down\t-\t%s\t%s\t-\n' "$peer" "$location" >> "$out"
        return
    fi

    start=$(date +%s%3N)
    if check_tcp "$ip" "$port" "$TIMEOUT_SECONDS"; then
        end=$(date +%s%3N)
        latency=$((end - start))
        printf 'up\t%s\t%s\t%s\t%s\n' "$latency" "$peer" "$location" "$ip" >> "$out"
    else
        printf 'down\t-\t%s\t%s\t%s\n' "$peer" "$location" "$ip" >> "$out"
    fi
}

echo "Fetching peer list from GitHub..." >&2
tree_json=$(fetch "$TREE_URL")
paths=$(grep -o '"path": *"[^"]*\.md"' <<< "$tree_json" | sed -E 's/"path": *"([^"]*)"/\1/')

declare -A location_of
peers=()

while IFS= read -r path; do
    [ -z "$path" ] && continue
    text=$(fetch "$RAW_BASE/$path")
    country_location=$(sed -E 's/\.md$//; s#/# \/ #g' <<< "$path")

    # Country files list peers grouped under a top-level "* City, description"
    # bullet (peer URIs are indented under it), so track the most recent city
    # bullet line-by-line to tag each peer with "Continent / Country / City".
    current_city=""
    while IFS= read -r line; do
        if [[ "$line" =~ ^\*[[:space:]]+([^,]+), ]]; then
            current_city=$(sed -E 's/^[[:space:]]+|[[:space:]]+$//g' <<< "${BASH_REMATCH[1]}")
        fi
        if [ -n "$current_city" ]; then
            location="$country_location / $current_city"
        else
            location="$country_location"
        fi
        while IFS= read -r peer; do
            [ -z "$peer" ] && continue
            if [ -z "${location_of[$peer]:-}" ]; then
                location_of[$peer]="$location"
                peers+=("$peer")
            fi
        done < <(grep -oE '(tcp|tls|socks|quic)://(\[[0-9a-fA-F:]+\]|[a-zA-Z0-9.-]+):[0-9]+' <<< "$line")
    done <<< "$text"
done <<< "$paths"

echo "${#peers[@]} unique peers found. Testing with up to $CONCURRENCY at a time..." >&2

results_file=$(mktemp)
running=0

for peer in "${peers[@]}"; do
    rest="${peer#*://}"
    if [[ "$rest" == \[* ]]; then
        host="${rest#\[}"
        host="${host%%\]*}"
        port="${rest##*:}"
    else
        host="${rest%:*}"
        port="${rest##*:}"
    fi
    [ -z "$port" ] && continue

    check_one "$peer" "${location_of[$peer]}" "$host" "$port" "$results_file" &
    running=$((running + 1))
    if [ "$running" -ge "$CONCURRENCY" ]; then
        wait -n
        running=$((running - 1))
    fi
done
wait

echo >&2
echo "=== Results, sorted by latency (unreachable last) ==="
sort -t$'\t' -k1,1r -k2,2n "$results_file" | while IFS=$'\t' read -r status latency peer location ip; do
    if [ "$status" = "up" ]; then
        printf '%6sms  %-50s %-20s %s\n' "$latency" "$peer" "$ip" "$location"
    else
        printf '%8s  %-50s %-20s %s\n' "TIMEOUT" "$peer" "$ip" "$location"
    fi
done

up_count=$(grep -c '^up' "$results_file")
total_count=$(wc -l < "$results_file")
echo >&2
echo "$up_count / $total_count peers reachable from this machine." >&2

if [ -n "$REPORT_URL" ]; then
    echo "Reporting results to $REPORT_URL ..." >&2
    payload=$(awk -F'\t' '
        BEGIN { printf "{\"results\":[" }
        { printf "%s{\"peer\":\"%s\",\"up\":%s,\"location\":\"%s\",\"ip\":\"%s\"}", (NR>1?",":""), $3, ($1=="up"?"true":"false"), $4, $5 }
        END { print "]}" }
    ' "$results_file")

    if command -v curl >/dev/null 2>&1; then
        curl -sS -X POST -H "Content-Type: application/json" -d "$payload" "$REPORT_URL"
    else
        wget -qO- --header="Content-Type: application/json" --post-data="$payload" "$REPORT_URL"
    fi
    echo
fi

# --- Suggestion: 4 fastest distinct servers, deduplicated by resolved IP ---
# Also excludes this machine's own addresses: if this box happens to be one of
# the listed public peers itself, it would trivially "win" on latency, which is
# a useless (and self-referential) suggestion.

declare -A is_local_ip
while IFS= read -r ip; do
    [ -n "$ip" ] && is_local_ip["$ip"]=1
done < <(ip -o addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1)

stats_json=""
if command -v python3 >/dev/null 2>&1; then
    stats_json=$(fetch "$STATS_URL" 2>/dev/null)
fi

get_uptime() {
    local peer="$1" out
    if [ -n "$stats_json" ]; then
        out=$(python3 -c "
import json, sys
try:
    data = json.loads(sys.argv[1])
except Exception:
    data = {}
info = data.get(sys.argv[2], {})
print(f\"{info.get('uptime', 1)}\t{info.get('checks', 0)}\")
" "$stats_json" "$peer" 2>/dev/null)
        if [ -n "$out" ]; then
            echo "$out"
            return
        fi
    fi
    echo -e "1\t0"
}

scoring_file=$(mktemp)
while IFS=$'\t' read -r status latency peer location ip; do
    [ "$status" = "up" ] || continue
    [ -n "${is_local_ip[$ip]:-}" ] && continue
    IFS=$'\t' read -r uptime checks < <(get_uptime "$peer")
    printf '%s\t%s\t%s\t%s\t%s\t%s\n' "${uptime:-1}" "$latency" "$peer" "$location" "$ip" "${checks:-0}" >> "$scoring_file"
done < "$results_file"

declare -A seen_ip
best_lines=()
while IFS=$'\t' read -r uptime latency peer location ip checks; do
    [ -n "${seen_ip[$ip]:-}" ] && continue
    seen_ip[$ip]=1
    best_lines+=("$uptime|$latency|$peer|$location|$ip|$checks")
    [ "${#best_lines[@]}" -ge 4 ] && break
done < <(sort -t$'\t' -k1,1rn -k2,2n "$scoring_file")

rm -f "$scoring_file" "$results_file"

echo
echo "=== 4 fastest distinct servers (deduplicated by resolved IP) ==="
if [ "${#best_lines[@]}" -eq 0 ]; then
    echo "(no reachable peers to suggest)"
else
    for line in "${best_lines[@]}"; do
        IFS='|' read -r uptime latency peer location ip checks <<< "$line"
        if [ "${checks:-0}" -gt 0 ] 2>/dev/null; then
            uptime_pct=$(awk -v u="$uptime" 'BEGIN{printf "%.0f", u*100}')
            echo "  $peer  (${latency}ms, ${uptime_pct}% uptime over 7d / $checks checks, $location)"
        else
            echo "  $peer  (${latency}ms, no uptime history yet, $location)"
        fi
    done

    echo
    echo "Suggested yggdrasil.conf snippet:"
    echo '"Peers": ['
    last_index=$(( ${#best_lines[@]} - 1 ))
    for i in "${!best_lines[@]}"; do
        IFS='|' read -r uptime latency peer location ip checks <<< "${best_lines[$i]}"
        sep=","
        [ "$i" -eq "$last_index" ] && sep=""
        echo "    \"${peer}\"${sep}"
    done
    echo ']'
fi
