#!/bin/bash
# resume-tunnel.sh — Resume the Cloudflare tunnel on brand-service start.
#
# Spawns cloudflared into its own transient systemd scope (cloudflared-<brand>.scope)
# under cloudflared.slice, completely decoupled from the brand service's cgroup.
# A brand-service restart can no longer reap the tunnel. Task 602.
#
# Called as ExecStartPre in maxy.service. All failures exit 0 — this script must
# never prevent maxy.service from starting.
#
# Dependencies: jq, cloudflared, systemd-run, systemctl

set -uo pipefail

# Derive configDir from brand.json. Fall back to .maxy if unreadable.
CONFIG_DIR=".maxy"
if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -f "$MAXY_PLATFORM_ROOT/config/brand.json" ]; then
  BRAND_CONFIG_DIR=$(jq -r '.configDir // empty' "$MAXY_PLATFORM_ROOT/config/brand.json" 2>/dev/null || true)
  if [ -n "$BRAND_CONFIG_DIR" ]; then
    CONFIG_DIR="$BRAND_CONFIG_DIR"
  fi
fi

STATE_FILE="$HOME/$CONFIG_DIR/cloudflared/tunnel.state"
LOG_DIR="$HOME/$CONFIG_DIR/logs"
LOG_FILE="$LOG_DIR/cloudflared.log"

log() {
  mkdir -p "$LOG_DIR"
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] resume-tunnel: $1" >> "$LOG_FILE"
}

# No state file — no tunnel configured. Normal boot.
[ -f "$STATE_FILE" ] || exit 0

log "reading state from $STATE_FILE (configDir=$CONFIG_DIR)"

if ! STATE=$(jq -r '.' "$STATE_FILE" 2>/dev/null); then
  log "WARNING: tunnel.state is not valid JSON — skipping resume"
  exit 0
fi

TUNNEL_ID=$(echo "$STATE" | jq -r '.tunnelId // empty')
DOMAIN=$(echo "$STATE" | jq -r '.domain // empty')
CONFIG_PATH=$(echo "$STATE" | jq -r '.configPath // empty')

if [ -z "$TUNNEL_ID" ]; then
  log "WARNING: tunnel.state has no tunnelId — skipping resume"
  exit 0
fi

# Derive scope unit name: strip leading dot from CONFIG_DIR.
# .maxy-code → cloudflared-maxy-code.scope
BRAND="${CONFIG_DIR#.}"
SCOPE_UNIT="cloudflared-${BRAND}.scope"

# Liveness check: query the scope unit state, not a recorded PID.
# The scope is owned by user-systemd, fully outside the brand service's cgroup.
if systemctl --user is-active "$SCOPE_UNIT" >/dev/null 2>&1; then
  SCOPE_PID=$(systemctl --user show "$SCOPE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
  log "Tunnel already running (scope $SCOPE_UNIT active, MainPID $SCOPE_PID) — skipping resume"
  exit 0
fi

CERT_PATH="$HOME/$CONFIG_DIR/cloudflared/cert.pem"
CLOUDFLARED_BIN=$(command -v cloudflared 2>/dev/null || true)

if [ -z "$CLOUDFLARED_BIN" ]; then
  log "ERROR: cloudflared binary not found — cannot resume tunnel"
  exit 0
fi

if [ -z "$CONFIG_PATH" ] || [ ! -f "$CONFIG_PATH" ] || [ ! -f "$CERT_PATH" ]; then
  log "ERROR: missing configPath or cert — cannot resume tunnel"
  exit 0
fi

log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."

# Capture log byte offset before spawning so the verification poll only
# matches lines written by THIS invocation, not lines from a prior run.
mkdir -p "$LOG_DIR"
LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)

# Spawn into a transient scope under cloudflared.slice.
# The scope is owned by user-systemd, not by the brand service's cgroup.
# systemd-run --scope blocks until cloudflared exits (it is the scope leader),
# so we background it. $? after & is always 0 (fork success); the verification
# gate below detects actual spawn failures via connection timeout.
# stdout/stderr of cloudflared append to LOG_FILE via the redirect.
systemd-run \
  --user \
  --scope \
  --quiet \
  --unit="$SCOPE_UNIT" \
  --slice=cloudflared \
  -p TimeoutStopSec=15 \
  -- \
  "$CLOUDFLARED_BIN" --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run \
  >> "$LOG_FILE" 2>&1 &

# Log spawn receipt so the operator can distinguish the scoped path from legacy nohup.
SCOPE_PID=$(systemctl --user show "$SCOPE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
log "spawned scope=$SCOPE_UNIT slice=cloudflared main-pid=$SCOPE_PID"

# Post-spawn connection verification gate.
# Polls only new log content (from LOG_OFFSET onwards) to avoid false positives
# from "Registered tunnel connection" lines written by a prior run.
# Overridable via RESUME_TUNNEL_VERIFY_DEADLINE for testing.
# On failure: log explicit ERROR and exit 0 — must not block brand-service start.
VERIFY_DEADLINE="${RESUME_TUNNEL_VERIFY_DEADLINE:-20}"
VERIFY_START=$(date +%s)
VERIFIED=0
while true; do
  if tail -c "+$((LOG_OFFSET + 1))" "$LOG_FILE" 2>/dev/null | grep -q "Registered tunnel connection"; then
    VERIFIED=1
    break
  fi
  NOW=$(date +%s)
  ELAPSED=$(( NOW - VERIFY_START ))
  if [ "$ELAPSED" -ge "$VERIFY_DEADLINE" ]; then
    break
  fi
  sleep 1
done

if [ "$VERIFIED" -eq 1 ]; then
  ELAPSED=$(( $(date +%s) - VERIFY_START ))
  CONN_COUNT=$(tail -c "+$((LOG_OFFSET + 1))" "$LOG_FILE" 2>/dev/null | grep -c "Registered tunnel connection" || echo "?")
  log "tunnel verified scope=$SCOPE_UNIT connections=$CONN_COUNT ms=$(( ELAPSED * 1000 ))"
else
  log "ERROR: tunnel scope started but no edge connection within ${VERIFY_DEADLINE}s — scope=$SCOPE_UNIT"
fi

# Update state file: keep pid/startedAt for observability, add scopeUnit as control signal.
NEW_STATE=$(echo "$STATE" | jq \
  --arg unit "$SCOPE_UNIT" \
  --argjson ts "$(date +%s)000" \
  '.scopeUnit = $unit | .startedAt = $ts' 2>/dev/null || echo "")

if [ -n "$NEW_STATE" ] && echo "$NEW_STATE" > "$STATE_FILE.tmp" 2>/dev/null && mv "$STATE_FILE.tmp" "$STATE_FILE" 2>/dev/null; then
  :
else
  rm -f "$STATE_FILE.tmp" 2>/dev/null
  log "WARNING: tunnel started but failed to update state file"
fi

exit 0
