#!/bin/sh

# network-restart 6.0.0
# Copyright (c)2017-2026 John Lawson & Sons
# All Rights Reserved
#
# Network connectivity watchdog.
#
# Recovery order:
#   1. Allow normal wpa_supplicant/networkd recovery.
#   2. Renew DHCP after a sustained outage.
#   3. Restart wpa_supplicant after a longer outage.
#   4. Restart systemd-networkd as a last resort.
#   5. Reboot only after 24 hours offline.
#
# This script is intended to be called periodically by a systemd timer.

set -eu
umask 077

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

TEST_ADDRESS="8.8.8.8"

PING_COUNT=2
PING_TIMEOUT=3

# Minimum time offline before each recovery stage.
DHCP_RENEW_AFTER=$((2 * 60))
WPA_RESTART_AFTER=$((10 * 60))
NETWORKD_RESTART_AFTER=$((30 * 60))
MAX_DOWNTIME=$((24 * 60 * 60))

# Minimum interval between repeated recovery actions.
DHCP_COOLDOWN=$((2 * 60))
WPA_COOLDOWN=$((10 * 60))
NETWORKD_COOLDOWN=$((30 * 60))

RECOVERY_DELAY=5

STATE_DIR="/run/network-restart"

LAST_OK_FILE="$STATE_DIR/last_connected"
OFFLINE_SINCE_FILE="$STATE_DIR/offline_since"

LAST_DHCP_FILE="$STATE_DIR/last_dhcp_renew"
LAST_WPA_FILE="$STATE_DIR/last_wpa_restart"
LAST_NETWORKD_FILE="$STATE_DIR/last_networkd_restart"

STATE_FILE="$STATE_DIR/state"

LOG_TAG="network-restart"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

log()
{
	logger -t "$LOG_TAG" "$*"
}

now_s()
{
	date +%s
}

read_timestamp()
{
	file="$1"
	default_value="${2:-0}"
	value="$default_value"

	if [ -f "$file" ]; then
		value="$(cat "$file" 2>/dev/null || echo "$default_value")"
	fi

	case "$value" in
		*[!0-9]*|"")
			value="$default_value"
			;;
	esac

	printf '%s\n' "$value"
}

write_timestamp()
{
	file="$1"
	now_s >"$file" 2>/dev/null || true
}

log_state()
{
	message="$*"
	previous=""

	if [ -f "$STATE_FILE" ]; then
		previous="$(cat "$STATE_FILE" 2>/dev/null || true)"
	fi

	if [ "$message" != "$previous" ]; then
		printf '%s\n' "$message" >"$STATE_FILE" 2>/dev/null || true
		log "$message"
	fi
}

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

unit_exists()
{
	unit="$1"

	systemctl list-unit-files "$unit" --no-legend 2>/dev/null |
		grep -q "^${unit}[[:space:]]"
}

iface_exists()
{
	ip link show dev "$1" >/dev/null 2>&1
}

iface_is_up()
{
	ip link show dev "$1" 2>/dev/null |
		grep -qE '<[^>]*UP[^>]*>'
}

iface_has_ipv4()
{
	ip -4 address show dev "$1" scope global 2>/dev/null |
		grep -q 'inet '
}

default_iface()
{
	ip -4 route show default 2>/dev/null |
		awk '
			/^default / {
				for (i = 1; i <= NF; ++i) {
					if ($i == "dev" && (i + 1) <= NF) {
						print $(i + 1)
						exit
					}
				}
			}
		'
}

detect_iface()
{
	iface="$(default_iface)"

	if [ -n "${iface:-}" ] && iface_exists "$iface"; then
		printf '%s\n' "$iface"
		return 0
	fi

	for iface in eth0 wlan0; do
		if iface_exists "$iface"; then
			printf '%s\n' "$iface"
			return 0
		fi
	done

	return 1
}

ping_iface()
{
	iface="$1"

	if ! iface_is_up "$iface"; then
		return 1
	fi

	if ! iface_has_ipv4 "$iface"; then
		return 1
	fi

	ping \
		-q \
		-I "$iface" \
		-c "$PING_COUNT" \
		-W "$PING_TIMEOUT" \
		"$TEST_ADDRESS" \
		>/dev/null 2>&1
}

action_due()
{
	stamp_file="$1"
	cooldown="$2"

	now="$(now_s)"
	last="$(read_timestamp "$stamp_file" 0)"

	[ $((now - last)) -ge "$cooldown" ]
}

offline_duration()
{
	now="$(now_s)"
	offline_since="$(read_timestamp "$OFFLINE_SINCE_FILE" "$now")"

	if [ "$offline_since" -gt "$now" ]; then
		offline_since="$now"
	fi

	printf '%s\n' $((now - offline_since))
}

mark_connected()
{
	now="$(now_s)"

	printf '%s\n' "$now" >"$LAST_OK_FILE" 2>/dev/null || true
	rm -f "$OFFLINE_SINCE_FILE" 2>/dev/null || true
}

mark_offline()
{
	if [ ! -f "$OFFLINE_SINCE_FILE" ]; then
		write_timestamp "$OFFLINE_SINCE_FILE"
	fi
}

# ---------------------------------------------------------------------------
# Recovery actions
# ---------------------------------------------------------------------------

bring_iface_up()
{
	iface="$1"

	if iface_is_up "$iface"; then
		return 0
	fi

	log "Bringing interface $iface up"
	ip link set dev "$iface" up >/dev/null 2>&1 || true
	sleep "$RECOVERY_DELAY"
}

renew_dhcp()
{
	iface="$1"

	if ! command_exists networkctl; then
		return 1
	fi

	log "Renewing DHCP configuration on $iface"

	write_timestamp "$LAST_DHCP_FILE"

	networkctl renew "$iface" >/dev/null 2>&1 || true
	sleep "$RECOVERY_DELAY"
}

restart_wpa_supplicant()
{
	iface="$1"

	if [ "$iface" != "wlan0" ]; then
		return 1
	fi

	log "Restarting wpa_supplicant for $iface"

	write_timestamp "$LAST_WPA_FILE"

	if unit_exists "wpa_supplicant@${iface}.service"; then
		systemctl restart "wpa_supplicant@${iface}.service" \
			>/dev/null 2>&1 || true
	elif unit_exists "wpa_supplicant.service"; then
		systemctl restart wpa_supplicant.service \
			>/dev/null 2>&1 || true
	else
		log "No wpa_supplicant service was found"
		return 1
	fi

	sleep "$RECOVERY_DELAY"
}

restart_networkd()
{
	log "Restarting systemd-networkd"

	write_timestamp "$LAST_NETWORKD_FILE"

	systemctl restart systemd-networkd.service \
		>/dev/null 2>&1 || true

	sleep "$RECOVERY_DELAY"
}

reboot_if_offline_too_long()
{
	duration="$1"

	if [ "$duration" -lt "$MAX_DOWNTIME" ]; then
		return 0
	fi

	log_state \
		"OFFLINE too long (${duration}s >= ${MAX_DOWNTIME}s); rebooting"

	systemctl reboot
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

mkdir -p "$STATE_DIR" 2>/dev/null || true

if [ ! -f "$LAST_OK_FILE" ]; then
	write_timestamp "$LAST_OK_FILE"
fi

iface="$(detect_iface || true)"

if [ -z "${iface:-}" ]; then
	mark_offline

	duration="$(offline_duration)"

	log_state "NOIFACE no supported network interface (${duration}s offline)"
	reboot_if_offline_too_long "$duration"

	exit 0
fi

bring_iface_up "$iface"

if ping_iface "$iface"; then
	mark_connected
	log_state "UP on $iface"
	exit 0
fi

mark_offline
duration="$(offline_duration)"

log_state "DOWN on $iface (${duration}s offline)"

# Give wpa_supplicant and systemd-networkd time to recover naturally.
if [ "$duration" -lt "$DHCP_RENEW_AFTER" ]; then
	reboot_if_offline_too_long "$duration"
	exit 0
fi

# Stage 1: renew DHCP after two minutes.
if [ "$duration" -ge "$DHCP_RENEW_AFTER" ] &&
	action_due "$LAST_DHCP_FILE" "$DHCP_COOLDOWN"; then
	renew_dhcp "$iface"

	if ping_iface "$iface"; then
		mark_connected
		log_state "UP on $iface after DHCP renewal"
		exit 0
	fi
fi

# Stage 2: restart wpa_supplicant after ten minutes.
if [ "$iface" = "wlan0" ] &&
	[ "$duration" -ge "$WPA_RESTART_AFTER" ] &&
	action_due "$LAST_WPA_FILE" "$WPA_COOLDOWN"; then
	restart_wpa_supplicant "$iface"

	if command_exists networkctl; then
		networkctl renew "$iface" >/dev/null 2>&1 || true
		sleep "$RECOVERY_DELAY"
	fi

	if ping_iface "$iface"; then
		mark_connected
		log_state "UP on $iface after wpa_supplicant restart"
		exit 0
	fi
fi

# Stage 3: restart networkd after thirty minutes.
if [ "$duration" -ge "$NETWORKD_RESTART_AFTER" ] &&
	action_due "$LAST_NETWORKD_FILE" "$NETWORKD_COOLDOWN"; then
	restart_networkd

	if ping_iface "$iface"; then
		mark_connected
		log_state "UP on $iface after systemd-networkd restart"
		exit 0
	fi
fi

reboot_if_offline_too_long "$duration"

exit 0
