#!/bin/bash
# alpha-routing — allow-listed root helper for the Traffic Routing screen.
# Materialises /var/www/alpha-private/routing.json (written by the panel as
# www-data) into nginx configs: one domain fronting many backend VPSs by
# URL path, by subdomain, or as a load balancer with passive failover.
# Every value read from the JSON is regex-validated HERE before it touches
# a root-written config — the panel's checks are convenience, these are law.
set -euo pipefail

SPEC=/var/www/alpha-private/routing.json
AVAIL=/etc/nginx/sites-available
ENABLED=/etc/nginx/sites-enabled
SNIPPET=/etc/nginx/conf.d/alpha-routing-map.conf
PREFIX=alpha-route-
EMAIL="admin@$(hostname -d 2>/dev/null || echo localhost)"
[ -f /etc/alpha-panel.conf ] && . /etc/alpha-panel.conf 2>/dev/null || true
[ -n "${ALPHA_DOMAIN:-}" ] && EMAIL="admin@${ALPHA_DOMAIN}"

RE_DOMAIN='^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$'
RE_TARGET='^([a-z0-9.-]{1,253}|\[[0-9a-f:]+\]):[0-9]{1,5}$'
RE_PATH='^/[A-Za-z0-9._/-]{0,120}$'
RE_SUB='^(@|[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)$'
RE_ID='^rt_[a-f0-9]{6}$'
RE_IP='^(([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?|[0-9a-fA-F:]{2,45}(/[0-9]{1,3})?)$'
RE_HOST='^([a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?)$'
RE_USER='^[a-z_][a-z0-9_-]{0,31}$'

# ---- HA proxy nodes: replicate generated configs to secondary servers ----
NODES=/var/www/alpha-private/routing-nodes.json     # written by the panel (www-data)
SYNCSTATE=/var/www/alpha-private/routing-sync.json  # written HERE (root), read by the panel
KEYDIR=/var/lib/alpha-routing
KEY=$KEYDIR/sync_key

ensure_key(){
  mkdir -p "$KEYDIR"; chmod 755 "$KEYDIR"
  if [ ! -f "$KEY" ]; then ssh-keygen -t ed25519 -N "" -C "alpha-routing-sync" -f "$KEY" >/dev/null 2>&1; fi
  chmod 600 "$KEY" 2>/dev/null || true; chmod 644 "$KEY.pub" 2>/dev/null || true
}

# Push the shared snippet, every ENABLED routing config (+ its symlink) and the
# alpha-route certs to one node; remote nginx -t gates the remote reload.
sync_one(){  # <host> <port> <user>
  local host="$1" port="$2" user="$3" f list="etc/nginx/conf.d/alpha-routing-map.conf"
  for f in "$ENABLED"/$PREFIX*.conf; do
    [ -e "$f" ] || continue
    list="$list etc/nginx/sites-available/$(basename "$f")"
  done
  for f in /etc/letsencrypt/live/$PREFIX* /etc/letsencrypt/archive/$PREFIX* /etc/letsencrypt/renewal/$PREFIX*.conf; do
    [ -e "$f" ] || continue; list="$list ${f#/}"
  done
  # shellcheck disable=SC2086
  tar czhf - -C / $list 2>/dev/null | timeout 25 ssh -i "$KEY" -p "$port" \
      -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
      -o UserKnownHostsFile="$KEYDIR/known_hosts" "$user@$host" \
      'rm -f /etc/nginx/sites-enabled/alpha-route-* /etc/nginx/sites-available/alpha-route-*.conf
       tar xzf - -C / 2>/dev/null
       for f in /etc/nginx/sites-available/alpha-route-*.conf; do [ -e "$f" ] && ln -sf "$f" /etc/nginx/sites-enabled/; done
       mkdir -p /var/log/alpha-routing
       nginx -t >/dev/null 2>&1 && systemctl reload nginx && echo SYNCED' 2>/dev/null \
    | grep -q SYNCED
}

# Sync every enabled node from the panel's registry; record results for the UI.
sync_nodes(){
  [ -f "$NODES" ] || return 0
  local n i host port user en out="{" first=1 okv ts
  n=$(jq -r '.nodes | length' "$NODES" 2>/dev/null) || n=0; [[ "$n" =~ ^[0-9]{1,2}$ ]] || n=0
  [ "$n" -gt 0 ] || return 0
  ensure_key
  ts=$(date -Is)
  for ((i=0; i<n; i++)); do
    en=$(jq -r "if .nodes[$i].enabled then 1 else 0 end" "$NODES" 2>/dev/null)
    host=$(jq -r ".nodes[$i].host // \"\"" "$NODES" 2>/dev/null)
    port=$(jq -r ".nodes[$i].port // 22" "$NODES" 2>/dev/null)
    user=$(jq -r ".nodes[$i].user // \"root\"" "$NODES" 2>/dev/null)
    [[ "$host" =~ $RE_HOST ]] || continue
    [[ "$port" =~ ^[0-9]{1,5}$ ]] || continue
    [[ "$user" =~ $RE_USER ]] || continue
    [ "$en" = 1 ] || continue
    if sync_one "$host" "$port" "$user"; then okv=true; else okv=false; fi
    [ $first = 1 ] || out="$out,"
    first=0
    out="$out\"$host\":{\"ok\":$okv,\"ts\":\"$ts\"}"
  done
  out="$out}"
  [ $first = 1 ] || { printf '%s\n' "$out" > "$SYNCSTATE"; chmod 644 "$SYNCSTATE"; }
  return 0
}

vdomain(){ [[ "${1:-}" =~ $RE_DOMAIN ]] || { echo "BAD_DOMAIN"; exit 2; }; }
die(){ echo "$1"; exit 2; }

# The websocket upgrade map + per-domain access log format live once at http
# level, shared by every domain. Rewritten whenever the wanted content drifts.
ensure_snippet(){
  local want
  want=$(cat <<'EOF'
# alpha-routing: shared websocket upgrade map + log format (managed — do not edit)
map $http_upgrade $alpha_rt_connection {
    default upgrade;
    ''      close;
}
log_format alpha_rt '$time_iso8601|$host|$status|$request_time|$upstream_addr|$upstream_response_time|$body_bytes_sent|$remote_addr|$request_method $uri';
EOF
)
  if [ ! -f "$SNIPPET" ] || [ "$(cat "$SNIPPET")" != "$want" ]; then printf '%s\n' "$want" > "$SNIPPET"; fi
}

LOGDIR=/var/log/alpha-routing
ensure_logging(){
  mkdir -p "$LOGDIR"; chmod 755 "$LOGDIR"
  local lr=/etc/logrotate.d/alpha-routing
  if [ ! -f "$lr" ]; then
    cat > "$lr" <<'EOF'
/var/log/alpha-routing/*.access.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    sharedscripts
    postrotate
        systemctl reload nginx >/dev/null 2>&1 || true
    endscript
}
EOF
  fi
}

spec_for(){ jq -c --arg d "$1" '.domains[] | select(.domain==$d)' "$SPEC" 2>/dev/null | head -1; }

# Refuse a hostname any OTHER vhost already serves — a routed domain must never
# shadow the panel host, a portal, or a site the panel manages elsewhere.
host_taken(){
  local d="$1" name="$2" f
  for f in "$ENABLED"/*; do
    [ -e "$f" ] || continue
    [[ "$(basename "$f")" == "$PREFIX$d.conf" ]] && continue
    grep -Eq "server_name[^;]*(^|[ \t])$name(\$|[ \t;])" "$f" 2>/dev/null && return 0
  done
  return 1
}

# Every server_name the spec will emit (apex, or each sub FQDN in subs mode).
spec_hostnames(){
  local d="$1" spec="$2"
  if [ "$(jq -r '.mode' <<<"$spec")" = subs ]; then
    jq -r '.backends[].sub' <<<"$spec" | while IFS= read -r s; do
      [[ "$s" =~ $RE_SUB ]] || continue
      [ "$s" = "@" ] && echo "$d" || echo "$s.$d"
    done
  else
    echo "$d"
  fi
}

proxy_common(){
  cat <<'EOF'
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $alpha_rt_connection;
        proxy_connect_timeout 5s;
        proxy_read_timeout 120s;
EOF
}

# Emit the ssl directives when this domain's cert exists (cert name = alpha-route-<domain>).
ssl_lines(){
  local d="$1" live="/etc/letsencrypt/live/$PREFIX$1"
  [ -f "$live/fullchain.pem" ] || return 1
  echo "    listen 443 ssl;"
  echo "    ssl_certificate $live/fullchain.pem;"
  echo "    ssl_certificate_key $live/privkey.pem;"
}

generate(){
  local d="$1" spec="$2" out="$3"
  local mode algo sticky enforce id n i
  mode=$(jq -r '.mode' <<<"$spec"); [[ "$mode" =~ ^(paths|subs|lb)$ ]] || die BAD_MODE
  id=$(jq -r '.id' <<<"$spec");     [[ "$id" =~ $RE_ID ]] || die BAD_ID
  algo=$(jq -r '.algorithm // "round_robin"' <<<"$spec"); [[ "$algo" =~ ^(round_robin|least_conn|ip_hash)$ ]] || die BAD_ALGO
  sticky=$(jq -r 'if .sticky then 1 else 0 end' <<<"$spec")
  enforce=$(jq -r 'if .https.enforce then 1 else 0 end' <<<"$spec")
  n=$(jq '.backends | length' <<<"$spec"); [ "$n" -ge 1 ] && [ "$n" -le 32 ] || die BAD_BACKENDS

  # validate every backend up front
  for ((i=0; i<n; i++)); do
    local t s p w
    t=$(jq -r ".backends[$i].target" <<<"$spec"); [[ "$t" =~ $RE_TARGET ]] || die "BAD_TARGET $t"
    s=$(jq -r ".backends[$i].scheme // \"http\"" <<<"$spec"); [[ "$s" =~ ^https?$ ]] || die BAD_SCHEME
    w=$(jq -r ".backends[$i].weight // 1" <<<"$spec"); [[ "$w" =~ ^[0-9]{1,3}$ ]] || die BAD_WEIGHT
    if [ "$mode" = paths ]; then p=$(jq -r ".backends[$i].path" <<<"$spec"); [[ "$p" =~ $RE_PATH ]] || die "BAD_PATH"; fi
    if [ "$mode" = subs ];  then p=$(jq -r ".backends[$i].sub"  <<<"$spec"); [[ "$p" =~ $RE_SUB  ]] || die "BAD_SUB"; fi
  done

  local sslok=0; ssl_lines "$d" >/dev/null 2>&1 && sslok=1

  # -- routing rules: chained maps, first match wins, beats mode routing --
  local nr rulevar="" rups="" rmaps="" rseen=" " ranyssl=0
  local rk rtype rname rmatch rval rbid rtgt rsch rsrc rnext rentry resc
  nr=$(jq -r '.rules | length' <<<"$spec" 2>/dev/null) || nr=0; [[ "$nr" =~ ^[0-9]{1,2}$ ]] || nr=0
  if [ "$nr" -gt 16 ]; then die BAD_RULES; fi
  if [ "$nr" -gt 0 ]; then
    for ((rk=nr-1; rk>=0; rk--)); do
      rtype=$(jq -r ".rules[$rk].type" <<<"$spec");  [[ "$rtype" =~ ^(header|cookie|arg|useragent|method)$ ]] || die BAD_RULE_TYPE
      rname=$(jq -r ".rules[$rk].name // \"\"" <<<"$spec")
      case "$rtype" in header|cookie|arg) [[ "$rname" =~ ^[A-Za-z0-9_-]{1,40}$ ]] || die BAD_RULE_NAME;; esac
      rmatch=$(jq -r ".rules[$rk].match" <<<"$spec"); [[ "$rmatch" =~ ^(equals|contains)$ ]] || die BAD_RULE_MATCH
      rval=$(jq -r ".rules[$rk].value" <<<"$spec")
      if [ "$rtype" = method ]; then [[ "$rval" =~ ^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$ ]] || die BAD_RULE_VALUE
      else [[ "$rval" =~ ^[A-Za-z0-9\ ._:/=+@-]{1,80}$ ]] || die BAD_RULE_VALUE; fi
      rbid=$(jq -r ".rules[$rk].backend" <<<"$spec"); [[ "$rbid" =~ ^b[a-f0-9]{6}$ ]] || die BAD_RULE_BACKEND
      rtgt=$(jq -r --arg b "$rbid" '[.backends[] | select(.id==$b)][0].target // ""' <<<"$spec")
      rsch=$(jq -r --arg b "$rbid" '[.backends[] | select(.id==$b)][0].scheme // "http"' <<<"$spec")
      [[ "$rtgt" =~ $RE_TARGET ]] || die BAD_RULE_BACKEND
      [[ "$rsch" =~ ^https?$ ]] || die BAD_RULE_BACKEND
      [ "$rsch" = https ] && ranyssl=1
      if [[ "$rseen" != *" $rbid "* ]]; then
        rups="${rups}upstream alpha_${id}_${rbid} { server $rtgt; }
"
        rseen="$rseen$rbid "
      fi
      case "$rtype" in
        header)    rsrc="\$http_$(printf '%s' "$rname" | tr 'A-Z-' 'a-z_')";;
        cookie)    rsrc="\$cookie_$rname";;
        arg)       rsrc="\$arg_$rname";;
        useragent) rsrc="\$http_user_agent";;
        method)    rsrc="\$request_method";;
      esac
      if [ "$rk" -eq $((nr-1)) ]; then rnext='""'; else rnext="\$alpha_${id}_r$((rk+1))"; fi
      if [ "$rmatch" = contains ]; then
        resc=$(printf '%s' "$rval" | sed 's/[.+]/\\&/g')
        rentry="\"~*$resc\" \"$rsch://alpha_${id}_${rbid}\";"
      else
        rentry="\"$rval\" \"$rsch://alpha_${id}_${rbid}\";"
      fi
      rmaps="${rmaps}map $rsrc \$alpha_${id}_r$rk {
    default $rnext;
    $rentry
}
"
    done
    rulevar="\$alpha_${id}_r0"
  fi

  # -- security block (absent on pre-batch-4 specs → safe defaults) --
  local rl rlrate rlburst ipmode probes hdrs mbody iplines="" nip ip
  rl=$(jq -r 'if .security.rl_enabled then 1 else 0 end' <<<"$spec")
  rlrate=$(jq -r '.security.rl_rate // 10' <<<"$spec");  [[ "$rlrate"  =~ ^[0-9]{1,5}$ ]] || die BAD_RL_RATE
  rlburst=$(jq -r '.security.rl_burst // 20' <<<"$spec"); [[ "$rlburst" =~ ^[0-9]{1,5}$ ]] || die BAD_RL_BURST
  ipmode=$(jq -r '.security.ip_mode // "off"' <<<"$spec"); [[ "$ipmode" =~ ^(off|block|allow)$ ]] || die BAD_IP_MODE
  probes=$(jq -r 'if .security.probes then 1 else 0 end' <<<"$spec")
  hdrs=$(jq -r 'if .security.headers then 1 else 0 end' <<<"$spec")
  mbody=$(jq -r '.security.max_body // 25' <<<"$spec"); [[ "$mbody" =~ ^[0-9]{1,5}$ ]] || die BAD_BODY
  if [ "$ipmode" != off ]; then
    nip=$(jq -r '.security.ips | length' <<<"$spec"); [[ "$nip" =~ ^[0-9]{1,3}$ ]] || die BAD_IPS
    if [ "$nip" -gt 64 ]; then die BAD_IPS; fi
    for ((i=0; i<nip; i++)); do
      ip=$(jq -r ".security.ips[$i]" <<<"$spec"); [[ "$ip" =~ $RE_IP ]] || die "BAD_IP $ip"
      if [ "$ipmode" = block ]; then iplines="$iplines    deny $ip;
"; else iplines="$iplines    allow $ip;
"; fi
    done
    if [ "$ipmode" = allow ] && [ -n "$iplines" ]; then iplines="$iplines    deny all;
"; fi
    if [ -z "$iplines" ]; then ipmode=off; fi
  fi

  # Emitted inside every proxying location: when a rule matched, its target
  # (an upstream defined above) wins over the location's own proxy_pass.
  rule_route(){
    if [ -n "$rulevar" ]; then
      echo "        if ($rulevar) { proxy_pass $rulevar; }"
      [ "$ranyssl" = 1 ] && echo "        proxy_ssl_server_name on;"
    fi
    return 0
  }

  {
    echo "# Managed by alpha-routing — regenerated from routing.json, edits are lost."
    if [ "$rl" = 1 ]; then
      echo "limit_req_zone \$binary_remote_addr zone=alpha_rl_$id:10m rate=${rlrate}r/s;"
    fi
    if [ -n "$rups" ]; then printf '%s' "$rups"; printf '%s' "$rmaps"; fi
    if [ "$mode" = lb ]; then
      local eff="$algo"; [ "$sticky" = 1 ] && eff=ip_hash
      local uscheme; uscheme=$(jq -r '[.backends[] | select(.enabled)][0].scheme // "http"' <<<"$spec")
      # Health-driven failover: the cron worker (bin/routing-health.php) keeps
      # /var/www/alpha-private/routing-health.json; when the spec opts in, an
      # unhealthy backend is marked `down` here — but never ALL of them (if
      # everything looks dead, trust nginx's passive checks instead).
      local hf=/var/www/alpha-private/routing-health.json hdown=" " bid en2 upflag encnt=0 hdcnt=0
      if [ "$(jq -r 'if .health.failover then 1 else 0 end' <<<"$spec")" = 1 ] && [ -f "$hf" ]; then
        for ((i=0; i<n; i++)); do
          en2=$(jq -r "if .backends[$i].enabled then 1 else 0 end" <<<"$spec")
          if [ "$en2" = 1 ]; then
            encnt=$((encnt+1))
            bid=$(jq -r ".backends[$i].id" <<<"$spec")
            if [[ "$bid" =~ ^b[a-f0-9]{6}$ ]]; then
              upflag=$(jq -r --arg k "${id}:${bid}" 'if .backends[$k].up == false then 1 else 0 end' "$hf" 2>/dev/null || echo 0)
              if [ "$upflag" = 1 ]; then hdown="$hdown$i "; hdcnt=$((hdcnt+1)); fi
            fi
          fi
        done
        if [ "$hdcnt" -ge "$encnt" ]; then hdown=" "; fi
      fi
      echo "upstream alpha_$id {"
      [ "$eff" = least_conn ] && echo "    least_conn;"
      [ "$eff" = ip_hash ]    && echo "    ip_hash;"
      for ((i=0; i<n; i++)); do
        local t w en bk line
        t=$(jq -r ".backends[$i].target" <<<"$spec"); w=$(jq -r ".backends[$i].weight // 1" <<<"$spec")
        en=$(jq -r "if .backends[$i].enabled then 1 else 0 end" <<<"$spec")
        bk=$(jq -r "if .backends[$i].backup then 1 else 0 end" <<<"$spec")
        line="    server $t max_fails=3 fail_timeout=30s"
        [ "$w" != 1 ] && [ "$eff" != ip_hash ] && line="$line weight=$w"
        [ "$bk" = 1 ] && [ "$eff" != ip_hash ] && line="$line backup"
        if [ "$en" = 0 ]; then line="$line down"
        elif [[ "$hdown" == *" $i "* ]]; then line="$line down"; fi
        echo "$line;"
      done
      echo "}"
    fi

    emit_server(){  # $1=server_name  $2=body-emitter fn
      echo "server {"
      echo "    listen 80;"
      [ $sslok = 1 ] && ssl_lines "$d"
      echo "    server_name $1;"
      echo "    access_log $LOGDIR/$d.access.log alpha_rt;"
      echo "    client_max_body_size ${mbody}m;"
      if [ "$rl" = 1 ]; then
        echo "    limit_req zone=alpha_rl_$id burst=$rlburst nodelay;"
        echo "    limit_req_status 429;"
      fi
      if [ "$ipmode" != off ]; then printf '%s' "$iplines"; fi
      if [ "$hdrs" = 1 ]; then
        echo "    add_header X-Frame-Options SAMEORIGIN always;"
        echo "    add_header X-Content-Type-Options nosniff always;"
        echo "    add_header Referrer-Policy strict-origin-when-cross-origin always;"
      fi
      if [ $sslok = 1 ] && [ "$enforce" = 1 ]; then
        echo '    if ($scheme = http) { return 301 https://$host$request_uri; }'
      fi
      if [ "$probes" = 1 ]; then
        echo '    location ~ /\.(env|git|svn|htaccess|htpasswd|DS_Store) { deny all; access_log off; }'
      fi
      $2
      echo "}"
    }

    if [ "$mode" = lb ]; then
      local uscheme; uscheme=$(jq -r '[.backends[] | select(.enabled)][0].scheme // "http"' <<<"$spec")
      body_lb(){
        echo "    location / {"
        rule_route
        echo "        proxy_pass $uscheme://alpha_$id;"
        [ "$uscheme" = https ] && echo "        proxy_ssl_server_name on;"
        proxy_common
        echo "    }"
      }
      emit_server "$d" body_lb
    elif [ "$mode" = paths ]; then
      body_paths(){
        local j tp sp pp ep
        for ((j=0; j<n; j++)); do
          tp=$(jq -r ".backends[$j].target" <<<"$spec"); sp=$(jq -r ".backends[$j].scheme // \"http\"" <<<"$spec")
          pp=$(jq -r ".backends[$j].path" <<<"$spec");   ep=$(jq -r "if .backends[$j].enabled then 1 else 0 end" <<<"$spec")
          echo "    location $pp {"
          if [ "$ep" = 1 ]; then
            rule_route
            echo "        proxy_pass $sp://$tp;"
            [ "$sp" = https ] && echo "        proxy_ssl_server_name on;"
            proxy_common
          else
            echo "        return 503;"
          fi
          echo "    }"
        done
      }
      emit_server "$d" body_paths
    else  # subs — one server block per subdomain
      local j tj sj bj ej name
      for ((j=0; j<n; j++)); do
        tj=$(jq -r ".backends[$j].target" <<<"$spec"); sj=$(jq -r ".backends[$j].scheme // \"http\"" <<<"$spec")
        bj=$(jq -r ".backends[$j].sub" <<<"$spec");    ej=$(jq -r "if .backends[$j].enabled then 1 else 0 end" <<<"$spec")
        name="$bj.$d"; [ "$bj" = "@" ] && name="$d"
        body_sub(){
          echo "    location / {"
          if [ "$ej" = 1 ]; then
            rule_route
            echo "        proxy_pass $sj://$tj;"
            [ "$sj" = https ] && echo "        proxy_ssl_server_name on;"
            proxy_common
          else
            echo "        return 503;"
          fi
          echo "    }"
        }
        emit_server "$name" body_sub
      done
    fi
  } > "$out"
}

a="${1:-}"; shift || true
case "$a" in
  apply)
    vdomain "${1:-}"; d="$1"
    ensure_snippet
    ensure_logging
    conf="$AVAIL/$PREFIX$d.conf"; link="$ENABLED/$PREFIX$d.conf"
    spec=$(spec_for "$d")
    if [ -z "$spec" ]; then rm -f "$link" "$conf"; nginx -t >/dev/null 2>&1 && systemctl reload nginx; sync_nodes || true; echo OK_REMOVED; exit 0; fi
    while IFS= read -r hn; do
      if host_taken "$d" "$hn"; then die "HOST_TAKEN — another site already serves $hn"; fi
    done < <(spec_hostnames "$d" "$spec")
    bak=""; [ -f "$conf" ] && { bak=$(mktemp); cp "$conf" "$bak"; }
    tmp=$(mktemp)
    generate "$d" "$spec" "$tmp"
    install -m 644 "$tmp" "$conf"; rm -f "$tmp"
    if [ "$(jq -r 'if .enabled then 1 else 0 end' <<<"$spec")" = 1 ]; then ln -sf "$conf" "$link"; else rm -f "$link"; fi
    if err=$(nginx -t 2>&1); then
      systemctl reload nginx; [ -n "$bak" ] && rm -f "$bak"; sync_nodes || true; echo OK
    else
      if [ -n "$bak" ]; then mv "$bak" "$conf"; else rm -f "$conf" "$link"; fi
      nginx -t >/dev/null 2>&1 && systemctl reload nginx || true
      echo "NGINX_REJECTED: $(echo "$err" | head -3)"; exit 3
    fi ;;
  remove)
    vdomain "${1:-}"; d="$1"
    rm -f "$ENABLED/$PREFIX$d.conf" "$AVAIL/$PREFIX$d.conf" "$LOGDIR/$d.access.log" "$LOGDIR/$d.access.log."*
    nginx -t >/dev/null 2>&1 && systemctl reload nginx
    certbot delete --cert-name "$PREFIX$d" --non-interactive >/dev/null 2>&1 || true
    sync_nodes || true
    echo OK ;;
  pubkey)
    ensure_key; cat "$KEY.pub" ;;
  nodes-sync)
    ensure_key; sync_nodes || true
    cat "$SYNCSTATE" 2>/dev/null || echo '{}' ;;
  cert)
    vdomain "${1:-}"; d="$1"
    spec=$(spec_for "$d"); [ -n "$spec" ] || die NO_SPEC
    args=(-d "$d")
    if [ "$(jq -r '.mode' <<<"$spec")" = subs ]; then
      args=()
      while IFS= read -r sub; do
        [[ "$sub" =~ $RE_SUB ]] || die BAD_SUB
        [ "$sub" = "@" ] && args+=(-d "$d") || args+=(-d "$sub.$d")
      done < <(jq -r '.backends[].sub' <<<"$spec")
    fi
    if certbot certonly --nginx "${args[@]}" --cert-name "$PREFIX$d" --non-interactive --agree-tos -m "$EMAIL" \
         --deploy-hook "systemctl reload nginx; /usr/local/sbin/alpha-routing nodes-sync" >/dev/null 2>&1; then
      echo OK
    else
      die "CERT_FAIL — check that DNS for the hostname(s) points at this server"
    fi ;;
  status)
    # one JSON line per routed domain: config present? enabled? cert on disk?
    echo '{'
    first=1
    while IFS= read -r d; do
      [[ "$d" =~ $RE_DOMAIN ]] || continue
      c=0; e=0; s=0
      [ -f "$AVAIL/$PREFIX$d.conf" ] && c=1
      [ -e "$ENABLED/$PREFIX$d.conf" ] && e=1
      [ -f "/etc/letsencrypt/live/$PREFIX$d/fullchain.pem" ] && s=1
      [ $first = 1 ] || echo ','
      first=0
      printf '  "%s": {"conf":%d,"enabled":%d,"cert":%d}' "$d" "$c" "$e" "$s"
    done < <(jq -r '.domains[].domain' "$SPEC" 2>/dev/null)
    echo ''; echo '}' ;;
  *) echo "usage: alpha-routing apply|remove|cert <domain> | status | pubkey | nodes-sync"; exit 1 ;;
esac
