#!/usr/bin/env bash
# PC → Nighthawk edge reconnect loop
# Goal: after router USB apply reboot, get 192.168.1.x + EDGE_OK + internet via NAT
# Usage:
#   ./Build/pc-nighthawk-reconnect-loop.sh
#   IFACE=enp10s0 EDGE=192.168.1.1 INTERVAL=5 ./Build/pc-nighthawk-reconnect-loop.sh
#   ONCE=1  … one pass only
# Stop: Ctrl-C or kill the pid file
set -u

IFACE="${IFACE:-enp10s0}"
EDGE="${EDGE:-192.168.1.1}"
EDGE_NET="${EDGE_NET:-192.168.1.}"
INTERVAL="${INTERVAL:-5}"
INET_TEST="${INET_TEST:-1.1.1.1}"
CONN="${CONN:-nighthawk-lan}"
ONCE="${ONCE:-0}"
LOG="${LOG:-/tmp/nighthawk-pc-reconnect.log}"
PIDF="${PIDF:-/tmp/nighthawk-pc-reconnect.pid}"
MAX_FAIL_STREAK="${MAX_FAIL_STREAK:-0}"   # 0 = forever

echo $$ >"$PIDF"
exec > >(tee -a "$LOG") 2>&1

ts() { date -Is; }
say() { echo "[$(ts)] $*"; }

cleanup() {
  say "stop · pid was $$"
  rm -f "$PIDF"
  exit 0
}
trap cleanup INT TERM

say "START PC nighthawk reconnect loop"
say "IFACE=$IFACE EDGE=$EDGE CONN=$CONN INTERVAL=${INTERVAL}s LOG=$LOG"
say "desk tip: modem→router WAN · PC→router LAN (port 2+ if port1 dead) · stick APPLY_NOW"

have() { command -v "$1" >/dev/null 2>&1; }

pick_iface() {
  if ip link show "$IFACE" >/dev/null 2>&1; then
    return 0
  fi
  # first non-lo ethernet UP
  local d
  d=$(ip -br link | awk '$1!="lo" && $2 ~ /UP|UNKNOWN/ {print $1; exit}')
  if [ -n "${d:-}" ]; then
    IFACE=$d
    say "auto IFACE=$IFACE"
  fi
}

link_up() {
  ip link set "$IFACE" up 2>/dev/null || true
  if have ethtool; then
    ethtool "$IFACE" 2>/dev/null | grep -q 'Link detected: yes' && return 0
  fi
  # carrier sysfs
  local c
  c=$(cat "/sys/class/net/$IFACE/carrier" 2>/dev/null || echo 0)
  [ "$c" = "1" ]
}

addr_on_edge() {
  ip -4 -br addr show dev "$IFACE" 2>/dev/null | grep -q "${EDGE_NET}"
}

edge_ping() {
  ping -c1 -W2 "$EDGE" >/dev/null 2>&1
}

inet_ping() {
  ping -c1 -W3 "$INET_TEST" >/dev/null 2>&1
}

http_edge() {
  if have curl; then
    curl -sS -m 3 -o /dev/null -w '%{http_code}' "http://${EDGE}/" 2>/dev/null | grep -qE '200|301|302|403|404'
  else
    return 1
  fi
}

show_state() {
  local a r
  a=$(ip -4 -br addr show dev "$IFACE" 2>/dev/null | awk '{print $3}')
  r=$(ip route show default 2>/dev/null | head -1)
  say "state IFACE=$IFACE addr=${a:-none}"
  say "state route: ${r:-none}"
}

nm_reconnect() {
  have nmcli || return 1
  # prefer named connection if present
  if nmcli -t -f NAME con show 2>/dev/null | grep -qx "$CONN"; then
    say "nmcli down/up $CONN on $IFACE"
    nmcli con down "$CONN" 2>/dev/null || true
    sleep 1
    nmcli con up "$CONN" ifname "$IFACE" 2>/dev/null \
      || nmcli dev connect "$IFACE" 2>/dev/null \
      || return 1
    return 0
  fi
  say "nmcli reapply / reconnect $IFACE"
  nmcli dev disconnect "$IFACE" 2>/dev/null || true
  sleep 1
  nmcli dev connect "$IFACE" 2>/dev/null || nmcli con up "$IFACE" 2>/dev/null || return 1
  return 0
}

dhcp_renew() {
  # kill stale public lease aggressively then request again
  if have dhclient; then
    say "dhclient -r/-1 $IFACE"
    sudo -n dhclient -r "$IFACE" 2>/dev/null || dhclient -r "$IFACE" 2>/dev/null || true
    sleep 1
    sudo -n dhclient -1 -v "$IFACE" 2>/dev/null || dhclient -1 -v "$IFACE" 2>/dev/null || true
    return 0
  fi
  if have dhcpcd; then
    say "dhcpcd -n $IFACE (renew)"
    sudo -n dhcpcd -k "$IFACE" 2>/dev/null || dhcpcd -k "$IFACE" 2>/dev/null || true
    sleep 1
    sudo -n dhcpcd -n "$IFACE" 2>/dev/null || dhcpcd -n "$IFACE" 2>/dev/null || true
    return 0
  fi
  return 1
}

flush_wrong_subnet() {
  # if we still hold a non-edge public address while trying LAN, drop addrs and retry
  if addr_on_edge; then
    return 0
  fi
  # only flush if link is up (cable may now be on router)
  link_up || return 0
  say "flush non-edge addrs on $IFACE (was public/stale)"
  # delete all v4 addresses
  while read -r line; do
    # e.g. inet 71.x.x.x/22
    set -- $line
    [ "${1:-}" = "inet" ] || continue
    sudo -n ip -4 addr del "$2" dev "$IFACE" 2>/dev/null \
      || ip -4 addr del "$2" dev "$IFACE" 2>/dev/null || true
  done < <(ip -4 -o addr show dev "$IFACE" 2>/dev/null)
}

attempt() {
  pick_iface
  say "── attempt ──"
  show_state

  if ! link_up; then
    say "NO LINK on $IFACE · plug PC into router LAN (not WAN) · waiting"
    ip link set "$IFACE" up 2>/dev/null || true
    return 1
  fi
  say "link UP"

  # if already perfect, celebrate and keep watching (or exit if ONCE)
  if addr_on_edge && edge_ping; then
    say "EDGE_OK · addr on ${EDGE_NET}* · ping $EDGE ok"
    if inet_ping; then
      say "INET_OK · $INET_TEST · internet via path"
    else
      say "EDGE_OK but no internet yet · WAN/modem may still be coming up"
    fi
    if http_edge; then
      say "HTTP_EDGE ok · http://${EDGE}/"
    fi
    show_state
    return 0
  fi

  flush_wrong_subnet
  nm_reconnect || true
  sleep 2
  dhcp_renew || true
  sleep 2

  show_state
  if addr_on_edge && edge_ping; then
    say "EDGE_OK after renew"
    inet_ping && say "INET_OK" || say "wait WAN/NAT"
    http_edge && say "HTTP_EDGE ok" || true
    return 0
  fi

  if edge_ping; then
    say "ping edge works but addr not ${EDGE_NET}* yet"
    return 1
  fi
  say "EDGE_FAIL · cable on LAN? router finished APPLY (~90s)? stick APPLY_NOW?"
  return 1
}

ok_streak=0
fail_streak=0
n=0
while true; do
  n=$((n + 1))
  say "loop #$n"
  if attempt; then
    ok_streak=$((ok_streak + 1))
    fail_streak=0
    say "SUCCESS streak=$ok_streak · monitoring every ${INTERVAL}s (Ctrl-C to stop)"
    if [ "$ONCE" = "1" ]; then
      say "ONCE=1 · exit success"
      cleanup
    fi
    # keep monitoring; if we lose edge, fight again
    sleep "$INTERVAL"
    if addr_on_edge && edge_ping; then
      continue
    fi
    say "lost edge · re-enter reconnect"
  else
    ok_streak=0
    fail_streak=$((fail_streak + 1))
    say "not yet · fail_streak=$fail_streak · sleep ${INTERVAL}s"
    if [ "$MAX_FAIL_STREAK" -gt 0 ] && [ "$fail_streak" -ge "$MAX_FAIL_STREAK" ]; then
      say "MAX_FAIL_STREAK hit · exit 1"
      rm -f "$PIDF"
      exit 1
    fi
    sleep "$INTERVAL"
  fi
done
