#!/bin/sh
# SecuBox Mesh Join Script
# Compatible with OpenWRT and Debian/Ubuntu
# Supports both Debian API and OpenWRT CGI masters
#
# Usage: sbx-mesh-join <master-ip> <token>
#    OR: sbx-mesh-join <invite-url>
#
# Examples:
#   sbx-mesh-join 192.168.1.1 abc123def456
#   sbx-mesh-join "http://192.168.1.1:7331/master-link/?token=abc123"

VERSION="1.4.0"
API_PORT="7331"

usage() {
    echo "SecuBox Mesh Join v$VERSION"
    echo ""
    echo "Usage:"
    echo "  $0 <master-ip> <token>     Join using IP and token"
    echo "  $0 <invite-url>            Join using invite URL"
    echo ""
    echo "Examples:"
    echo "  $0 192.168.1.1 abc123def456"
    echo "  $0 'http://192.168.1.1:7331/master-link/?token=abc123'"
    echo ""
    echo "Supports:"
    echo "  - SecuBox Debian masters (API on port $API_PORT)"
    echo "  - SecuBox OpenWRT masters (CGI on port 80)"
    exit 1
}

# TRANSPORT DU PARENT (#2536). Un parent LOCAL (RFC 1918, boucle locale, lien local, *.local) se joint en HTTP sur le port du module,
# comme avant. Un parent DISTANT (la box l'appelle a travers une Freebox : admin.gk2.secubox.in) se joint en HTTPS par le nginx du
# parent, certificat VERIFIE (jamais -k) : le jeton ne voyage pas en clair sur Internet, et le port du module est filtre depuis l'exterieur.
es_adresse_locale() {
    case "$1" in
        10.*|192.168.*|127.*|169.254.*|localhost|*.local) return 0 ;;
        172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;;
    esac
    return 1
}
api_base_pour() {
    if es_adresse_locale "$1"; then echo "http://$1:$API_PORT"; else echo "https://$1"; fi
}

# Parse arguments
if [ $# -eq 1 ]; then
    # Single argument: URL
    URL="$1"
    # Extract master IP and token from URL
    MASTER_IP=$(echo "$URL" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
    TOKEN=$(echo "$URL" | grep -oE 'token=[^&]+' | cut -d= -f2)
    if [ -z "$MASTER_IP" ] || [ -z "$TOKEN" ]; then
        echo "ERROR: Could not parse URL"
        usage
    fi
elif [ $# -eq 2 ]; then
    MASTER_IP="$1"
    TOKEN="$2"
    # `-` : le jeton vient de l'environnement (SBX_MESH_TOKEN), pas de la ligne de commande — la jonction automatique (#2475) ne l'expose pas dans `ps`.
    [ "$TOKEN" = "-" ] && TOKEN="${SBX_MESH_TOKEN:-}"
    [ -z "$TOKEN" ] && usage
else
    usage
fi

API_BASE="$(api_base_pour "$MASTER_IP")"

# Detect system
detect_system() {
    if [ -f /etc/openwrt_release ]; then
        echo "openwrt"
    elif [ -f /etc/debian_version ]; then
        echo "debian"
    elif [ -f /etc/alpine-release ]; then
        echo "alpine"
    else
        echo "linux"
    fi
}

# Get node fingerprint
get_fingerprint() {
    local fp_file=""

    case "$SYSTEM" in
        openwrt)
            fp_file="/etc/secubox/node.id"
            if [ ! -f "$fp_file" ]; then
                mkdir -p /etc/secubox
                if [ -f /sys/class/net/br-lan/address ]; then
                    echo "owrt-$(cat /sys/class/net/br-lan/address | tr -d ':')" > "$fp_file"
                else
                    echo "owrt-$(cat /proc/sys/kernel/random/uuid | cut -d- -f1)" > "$fp_file"
                fi
            fi
            ;;
        *)
            fp_file="/var/lib/secubox/p2p/node.id"
            if [ ! -f "$fp_file" ]; then
                mkdir -p /var/lib/secubox/p2p
                if [ -f /sys/class/net/eth0/address ]; then
                    echo "sbx-$(cat /sys/class/net/eth0/address | tr -d ':')" > "$fp_file"
                else
                    echo "sbx-$(hostname | md5sum | cut -c1-12)" > "$fp_file"
                fi
            fi
            ;;
    esac

    cat "$fp_file"
}

# Get hostname
get_hostname() {
    case "$SYSTEM" in
        openwrt)
            uci get system.@system[0].hostname 2>/dev/null || hostname
            ;;
        *)
            hostname
            ;;
    esac
}

# Get local IP
get_local_ip() {
    case "$SYSTEM" in
        openwrt)
            ip -4 addr show br-lan 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
            ;;
        *)
            ip route get 1 2>/dev/null | grep -oP 'src \K[\d.]+' || echo "unknown"
            ;;
    esac
}

# HTTP POST using wget or curl
http_post() {
    local url="$1"
    local data="$2"

    if command -v curl >/dev/null 2>&1; then
        curl -s --connect-timeout 5 -X POST -H "Content-Type: application/json" -d "$data" "$url" 2>/dev/null
    elif command -v wget >/dev/null 2>&1; then
        wget -qO- --timeout=5 --post-data="$data" --header="Content-Type: application/json" "$url" 2>/dev/null
    else
        echo '{"error":"no http client"}'
        return 1
    fi
}

# HTTP GET using wget or curl
http_get() {
    local url="$1"

    if command -v curl >/dev/null 2>&1; then
        curl -s --connect-timeout 3 "$url" 2>/dev/null
    elif command -v wget >/dev/null 2>&1; then
        wget -qO- --timeout=3 "$url" 2>/dev/null
    else
        echo ""
        return 1
    fi
}

# Detect master type (Debian API or OpenWRT CGI)
detect_master_type() {
    local ip="$1"

    # Try Debian API first
    local base; base="$(api_base_pour "$ip")"
    local resp=$(http_get "$base/api/v1/p2p/master-link/status")
    if echo "$resp" | grep -q '"role"'; then
        echo "debian"
        return 0
    fi

    # Un parent distant (HTTPS) n'est jamais interroge en clair : pas de routes OpenWRT (#2536).
    es_adresse_locale "$ip" || { echo "unknown"; return 0; }

    # Try OpenWRT CGI
    resp=$(http_get "http://$ip/cgi-bin/masterlink/status")
    if echo "$resp" | grep -q '"role"'; then
        echo "openwrt-cgi"
        return 0
    fi

    # Try OpenWRT on custom port
    resp=$(http_get "http://$ip:$API_PORT/cgi-bin/masterlink/status")
    if echo "$resp" | grep -q '"role"'; then
        echo "openwrt-cgi-port"
        return 0
    fi

    echo "unknown"
}

# Join via Debian API
join_debian_api() {
    http_post "$API_BASE/api/v1/p2p/master-link/join" "$JSON_DATA"
}

# Join via OpenWRT CGI (port 80)
join_openwrt_cgi() {
    http_post "http://$MASTER_IP/cgi-bin/masterlink/join" "$JSON_DATA"
}

# Join via OpenWRT CGI (custom port)
join_openwrt_cgi_port() {
    http_post "http://$MASTER_IP:$API_PORT/cgi-bin/masterlink/join" "$JSON_DATA"
}

# Save success config
save_config() {
    case "$SYSTEM" in
        openwrt)
            mkdir -p /etc/secubox
            echo "$MASTER_IP" > /etc/secubox/master.ip
            echo "master-link" > /etc/secubox/mesh.mode
            echo "$MASTER_TYPE" > /etc/secubox/master.type
            ;;
        *)
            mkdir -p /var/lib/secubox/p2p
            cat > /var/lib/secubox/p2p/mesh.json << CONF
{"upstream":"$MASTER_IP","role":"peer","master_type":"$MASTER_TYPE"}
CONF
            ;;
    esac
}

# Main
SYSTEM=$(detect_system)
FINGERPRINT=$(get_fingerprint)
HOSTNAME=$(get_hostname)
ADDRESS=$(get_local_ip)

echo "╔══════════════════════════════════════════════════╗"
echo "║         SecuBox Mesh Join v$VERSION              ║"
echo "╠══════════════════════════════════════════════════╣"
echo "║ System:      $SYSTEM"
echo "║ Fingerprint: $FINGERPRINT"
echo "║ Hostname:    $HOSTNAME"
echo "║ Address:     $ADDRESS"
echo "║ Master:      $MASTER_IP"
echo "╚══════════════════════════════════════════════════╝"
echo ""

# ── CLÉ WIREGUARD DU PAIR (#1360) ─────────────────────────────────────────
# La jonction n'échangeait AUCUNE clé : le maître inscrivait le pair à son
# registre, et aucun tunnel ne pouvait se former. On génère (ou réutilise) la
# clé du pair et on l'envoie ; la clé privée ne quitte jamais cette machine.
WG_STATE=/var/lib/secubox/p2p/wg_mesh.json
WG_PORT=51822
WG_PUB=""
if [ "$SYSTEM" != "openwrt" ] && command -v wg >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
    mkdir -p /var/lib/secubox/p2p
    WG_PUB=$(python3 - "$WG_STATE" "$WG_PORT" <<'PY2'
import json, os, subprocess, sys
path, port = sys.argv[1], int(sys.argv[2])
try:
    st = json.load(open(path))
except Exception:
    st = {"peers": []}
if not st.get("private_key"):
    st["private_key"] = subprocess.run(["wg", "genkey"], capture_output=True,
                                       text=True, check=True).stdout.strip()
st["public_key"] = subprocess.run(["wg", "pubkey"], input=st["private_key"],
                                  capture_output=True, text=True, check=True).stdout.strip()
st.setdefault("listen_port", port)
st.setdefault("peers", [])
st["role"] = "peer"
old = os.umask(0o077)
tmp = path + ".tmp"
json.dump(st, open(tmp, "w"), indent=2)
os.umask(old)
os.replace(tmp, path)
print(st["public_key"])
PY2
    ) || WG_PUB=""
    # Le service p2p tourne sous `secubox` et lit cet état.
    chown secubox:secubox "$WG_STATE" 2>/dev/null || true
    chmod 600 "$WG_STATE" 2>/dev/null || true
fi

# Build join request
if [ -n "$WG_PUB" ]; then
JSON_DATA=$(cat <<EOF
{"token":"$TOKEN","fingerprint":"$FINGERPRINT","hostname":"$HOSTNAME","address":"$ADDRESS","wg_public_key":"$WG_PUB","wg_port":$WG_PORT}
EOF
)
else
JSON_DATA=$(cat <<EOF
{"token":"$TOKEN","fingerprint":"$FINGERPRINT","hostname":"$HOSTNAME","address":"$ADDRESS"}
EOF
)
fi

# Detect master type
echo "Detecting master type..."
MASTER_TYPE=$(detect_master_type "$MASTER_IP")
echo "  Master type: $MASTER_TYPE"
echo ""

# Try to join based on master type
echo "Joining mesh network..."
RESPONSE=""

case "$MASTER_TYPE" in
    debian)
        echo "  → Trying Debian API (port $API_PORT)..."
        RESPONSE=$(join_debian_api)
        ;;
    openwrt-cgi)
        echo "  → Trying OpenWRT CGI (port 80)..."
        RESPONSE=$(join_openwrt_cgi)
        ;;
    openwrt-cgi-port)
        echo "  → Trying OpenWRT CGI (port $API_PORT)..."
        RESPONSE=$(join_openwrt_cgi_port)
        ;;
    unknown)
        echo "  → Master type unknown, trying all methods..."
        # Try Debian API first
        RESPONSE=$(join_debian_api)
        if echo "$RESPONSE" | grep -q '"status"'; then
            MASTER_TYPE="debian"
        else
            # Try OpenWRT CGI port 80
            RESPONSE=$(join_openwrt_cgi)
            if echo "$RESPONSE" | grep -q '"status"'; then
                MASTER_TYPE="openwrt-cgi"
            else
                # Try OpenWRT CGI custom port
                RESPONSE=$(join_openwrt_cgi_port)
                if echo "$RESPONSE" | grep -q '"status"'; then
                    MASTER_TYPE="openwrt-cgi-port"
                fi
            fi
        fi
        ;;
esac

# Parse response
#
# `"success":true` NE VEUT PAS DIRE APPROUVÉ (#1360). Le maître le renvoie
# aussi pour une demande MISE EN ATTENTE — le script annonçait alors
# « SUCCESS … APPROVED » pour une jonction que personne n'avait acceptée. Seul
# le statut fait foi ; `success` seul ne vaut que pour les anciens maîtres
# OpenWrt, qui ne renvoient pas de statut.
if echo "$RESPONSE" | grep -q '"status":"approved"' || \
   { echo "$RESPONSE" | grep -q '"success":true' && ! echo "$RESPONSE" | grep -q '"status"'; }; then
    echo ""
    echo "✓ SUCCESS: Joined mesh network!"
    echo "  Status: APPROVED"
    DEPTH=$(echo "$RESPONSE" | grep -o '"depth":[0-9]*' | cut -d':' -f2)
    [ -n "$DEPTH" ] && echo "  Depth: $DEPTH"
    echo "  Master type: $MASTER_TYPE"

    # Save config
    save_config

    # ── MONTER LE TUNNEL, ET LE PROUVER (#1360) ───────────────────────────
    # « Joined mesh network » ne décrivait que l'inscription au registre : le
    # tunnel n'existait pas. On applique ce que le maître a renvoyé, puis on
    # VÉRIFIE par un ping à travers le tunnel — le seul verdict qui compte.
    if [ -n "$WG_PUB" ] && echo "$RESPONSE" | grep -q '"master_public_key"'; then
        MESH_IP=$(printf '%s' "$RESPONSE" | python3 -c '
import json, sys, os
path, master_ip = sys.argv[1], sys.argv[2]
wg = json.load(sys.stdin)["wg"]
st = json.load(open(path))
st.update({
    "address": wg["address"],
    "network": wg["network"],
    "role": "peer",
    "enabled": True,
    "peers": [{
        "public_key": wg["master_public_key"],
        "endpoint": "%s:%d" % (master_ip, int(wg["master_port"])),
        "allowed_ips": wg["network"],
        "mesh_ip": wg["master_mesh_ip"],
        "name": "master",
    }],
})
old = os.umask(0o077)
tmp = path + ".tmp"
json.dump(st, open(tmp, "w"), indent=2)
os.umask(old)
os.replace(tmp, path)
print(wg["master_mesh_ip"])
' "$WG_STATE" "$MASTER_IP") || MESH_IP=""
        chown secubox:secubox "$WG_STATE" 2>/dev/null || true
        if [ -n "$MESH_IP" ] && command -v sbx-mesh-up >/dev/null 2>&1; then
            echo "  Mesh address: $(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["address"])' "$WG_STATE")"
            sbx-mesh-up >/dev/null 2>&1 || echo "  ! sbx-mesh-up failed (run it as root to see why)"
            if ping -c 3 -W 2 "$MESH_IP" >/dev/null 2>&1; then
                echo "  Tunnel: UP — master reachable at $MESH_IP through wg-mesh"
            else
                echo "  Tunnel: NOT ESTABLISHED — enrolled, but no traffic to $MESH_IP yet"
                echo "          (master applies the peer asynchronously; retry: ping $MESH_IP)"
            fi
        fi
    elif [ -n "$WG_PUB" ]; then
        echo "  ! Master returned no WireGuard offer: enrolled WITHOUT a tunnel"
    fi

    echo ""
    echo "Master IP saved. You are now part of the mesh."
    exit 0

elif echo "$RESPONSE" | grep -q '"status":"pending"'; then
    echo ""
    echo "⏳ PENDING: Waiting for master approval"
    echo "  Your fingerprint: $FINGERPRINT"
    echo ""
    echo "  Ask the master admin to approve your request."
    exit 0

else
    echo ""
    echo "✗ FAILED to join mesh"
    echo "  Response: $RESPONSE"
    echo ""
    echo "Troubleshooting:"
    echo "  1. Check if master is reachable: ping $MASTER_IP"
    echo "  2. For Debian master: curl http://$MASTER_IP:$API_PORT/api/v1/p2p/master-link/status"
    echo "  3. For OpenWRT master: curl http://$MASTER_IP/cgi-bin/masterlink/status"
    echo "  4. Ensure master-link is installed on the master device"
    exit 1
fi
