#!/bin/sh
# kit-connect daemon — manages Tailscale + reverse SSH tunnel.
# Runs under procd supervision (auto-respawn on crash).
# Config: /etc/config/kitconnect (UCI) + hub-assigned settings.
# One package, all GL routers. Hub assigns ports and keys.

PKG="kit-connect"
STATE_DIR="/var/run/kitconnect"
CONF="/etc/config/kitconnect"
KEY="/etc/kitconnect/connect_id_ed25519"
PID_FILE="$STATE_DIR/daemon.pid"
TUNNEL_PID="$STATE_DIR/tunnel.pid"
LOG_TAG="kit-connect"

log() {
	logger -t "$LOG_TAG" -p daemon.info "$*"
}

warn() {
	logger -t "$LOG_TAG" -p daemon.warn "WARN: $*"
}

mkdir -p "$STATE_DIR"
chmod 600 "$KEY" 2>/dev/null
echo "$$" > "$PID_FILE"

# ── Config loader (UCI → shell vars) ──────────────────────────
load_config() {
	_uci() { uci -q get "kitconnect.main.${1}" 2>/dev/null || echo "${2}"; }

	DEVICE_ID=$(_uci device_id "")
	[ -z "$DEVICE_ID" ] && DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || hostname | sed 's/[^a-zA-Z0-9_-]//g')

	HUB_HOST=$(_uci hub_host "162.243.83.36")
	HUB_PORT=$(_uci hub_port "8080")

	TUNNEL_ENABLE=$(_uci tunnel_enable "1")
	TUNNEL_REMOTE_HOST=$(_uci tunnel_remote_host "162.243.83.36")
	TUNNEL_REMOTE_USER=$(_uci tunnel_remote_user "node")
	TUNNEL_REMOTE_SSH_PORT=$(_uci tunnel_remote_ssh_port "22")
	TUNNEL_REMOTE_PORT=$(_uci tunnel_remote_port "0")
	TUNNEL_LOCAL_PORT=$(_uci tunnel_local_port "2223")
	TUNNEL_RETRY_DELAY=$(_uci tunnel_retry_delay "30")

	TAILSCALE_ENABLE=$(_uci tailscale_enable "1")
	TAILSCALE_AUTH_KEY=$(_uci tailscale_auth_key "")
	TAILSCALE_HOSTNAME=$(_uci tailscale_hostname "")
	[ -z "$TAILSCALE_HOSTNAME" ] && TAILSCALE_HOSTNAME="$DEVICE_ID"

	WATCHDOG_INTERVAL=$(_uci watchdog_interval "60")
	FORWARD_FAIL_LIMIT=$(_uci forward_fail_limit "2")

	# Binary detection
	TAILSCALE_BIN=$(command -v tailscale 2>/dev/null || echo "/usr/sbin/tailscale")
	TAILSCALED_BIN=$(command -v tailscaled 2>/dev/null || echo "/usr/sbin/tailscaled")
}

# ── Start Tailscale ────────────────────────────────────────────
start_tailscale() {
	[ "$TAILSCALE_ENABLE" != "1" ] && { log "tailscale: disabled in config"; return 0; }
	[ -z "$TAILSCALE_AUTH_KEY" ] && { log "tailscale: no auth key — skipping"; return 0; }
	! command -v "$TAILSCALE_BIN" >/dev/null 2>&1 && { log "tailscale: binary not found"; return 1; }

	# Already connected?
	if "$TAILSCALE_BIN" status >/dev/null 2>&1; then
		log "tailscale: already connected ($($TAILSCALE_BIN ip -4 2>/dev/null || echo no-ip))"
		return 0
	fi

	log "tailscale: starting daemon..."
	"$TAILSCALED_BIN" --tun=userspace-networking >/dev/null 2>&1 &
	sleep 3

	log "tailscale: authenticating..."
	"$TAILSCALE_BIN" up \
		--auth-key "$TAILSCALE_AUTH_KEY" \
		--hostname "${TAILSCALE_HOSTNAME}" \
		--accept-routes=false \
		--accept-dns=false 2>&1 | while read -r line; do log "tailscale: $line"; done

	ts_ip=$("$TAILSCALE_BIN" ip -4 2>/dev/null || echo "unknown")
	log "tailscale: connected — IP=${ts_ip}"
}

# ── Start reverse SSH tunnel ───────────────────────────────────
start_tunnel() {
	[ "$TUNNEL_ENABLE" != "1" ] && { log "tunnel: disabled"; return 0; }
	[ "$TUNNEL_REMOTE_PORT" = "0" ] && { register_with_hub; load_config; }
	[ "$TUNNEL_REMOTE_PORT" = "0" ] && { log "tunnel: no port assigned — skipping"; return 0; }

	log "tunnel: opening R:0.0.0.0:${TUNNEL_REMOTE_PORT} → 127.0.0.1:${TUNNEL_LOCAL_PORT}"

	ssh \
		-o StrictHostKeyChecking=no \
		-o UserKnownHostsFile=/dev/null \
		-o ServerAliveInterval=15 \
		-o ServerAliveCountMax=3 \
		-o ExitOnForwardFailure=yes \
		-o BatchMode=yes \
		-p "${TUNNEL_REMOTE_SSH_PORT}" \
		-N \
		-R "0.0.0.0:${TUNNEL_REMOTE_PORT}:127.0.0.1:${TUNNEL_LOCAL_PORT}" \
		-i "$KEY" \
		"${TUNNEL_REMOTE_USER}@${TUNNEL_REMOTE_HOST}" \
		2>"$STATE_DIR/ssh_err" &

	SSH_PID=$!
	echo "$SSH_PID" > "$TUNNEL_PID"
	log "tunnel: SSH PID=${SSH_PID} port=${TUNNEL_REMOTE_PORT}"

	# Watchdog loop — monitors tunnel health
	_fail_count=0
	while kill -0 "$SSH_PID" 2>/dev/null; do
		sleep "$WATCHDOG_INTERVAL"
		kill -0 "$SSH_PID" 2>/dev/null || break

		if timeout 10 ssh \
			-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
			-o ConnectTimeout=5 -o BatchMode=yes \
			-p "${TUNNEL_REMOTE_SSH_PORT}" -i "$KEY" \
			"${TUNNEL_REMOTE_USER}@${TUNNEL_REMOTE_HOST}" \
			"timeout 3 bash -c 'echo >/dev/tcp/127.0.0.1/${TUNNEL_REMOTE_PORT}' 2>/dev/null" \
			2>/dev/null; then
			[ "$_fail_count" -gt 0 ] && log "tunnel: forward recovered after ${_fail_count} checks"
			_fail_count=0
		else
			_fail_count=$((_fail_count + 1))
			warn "tunnel: forward check failed (${_fail_count}/${FORWARD_FAIL_LIMIT})"
			[ "$_fail_count" -ge "$FORWARD_FAIL_LIMIT" ] && break
		fi
	done

	kill "$SSH_PID" 2>/dev/null
	rm -f "$TUNNEL_PID"
	log "tunnel: exited — will retry in ${TUNNEL_RETRY_DELAY}s"
	sleep "$TUNNEL_RETRY_DELAY"
	return 1
}

# ── Hub registration — get assigned port ───────────────────────
register_with_hub() {
	[ "$TUNNEL_REMOTE_PORT" != "0" ] && { return 0; }

	log "hub: registering device ${DEVICE_ID}..."

	resp=$(curl -s --connect-timeout 10 --max-time 15 \
		"http://${HUB_HOST}:${HUB_PORT}/api/register/${DEVICE_ID}" 2>/dev/null) || true

	[ -z "$resp" ] && { warn "hub: unreachable"; return 1; }

	# Parse JSON response (no jq dependency — grep + cut)
	port=$(echo "$resp" | grep -o '"tunnel_port"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*')
	tskey=$(echo "$resp" | grep -o '"tailscale_auth_key"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)

	if [ -n "$port" ] && [ "$port" != "0" ]; then
		uci set kitconnect.main.tunnel_remote_port="$port"
		uci commit kitconnect
		log "hub: assigned port ${port}"
		echo "$port" > "$STATE_DIR/hub_port"
	fi

	if [ -n "$tskey" ] && [ "$tskey" != "$TAILSCALE_AUTH_KEY" ]; then
		uci set kitconnect.main.tailscale_auth_key="$tskey"
		uci commit kitconnect
		log "hub: updated Tailscale key"
	fi
}

# ── Main loop (one cycle — procd respawns on exit) ────────────
log "EVENT=DAEMON_START device=${DEVICE_ID}"
load_config

# Ensure device-id file exists
mkdir -p /etc/busrouter
[ ! -f /etc/busrouter/device-id ] && echo "$DEVICE_ID" > /etc/busrouter/device-id

# Register with hub (non-fatal — retries next cycle)
register_with_hub 2>/dev/null || true

# Start Tailscale (runs in background, stays up)
start_tailscale 2>/dev/null || true

# Start tunnel (blocks until failure, then exits — procd restarts)
start_tunnel 2>/dev/null || true

log "EVENT=DAEMON_EXIT device=${DEVICE_ID}"
exit 0
