#!/bin/bash
# node-agent 节点 host 自更新（IP 变更时本地重配 + 上报 api）
#
# 演进自 Hiddify 版 host-update.sh：保留「取IP / 比较 / 上报 api」骨架，
# 把 Hiddify 专属的「改 panel SQLite domain 表 + 跑 hiddify install.sh」替换为
# node-agent 的本地重配：重签 {新ip}.sslip.io 证书 + 改 nginx/xray/agent 的 sslip 域名与
# public_ip + 重启 nginx/xray/agent，最后按契约 host {ip}:{port} 上报 api。
#
# 设计要点：
#   - 节点对 api 的契约 host = {ip}:{AGENT_API_PORT}（nginx 反代到本机 agent:8080）。
#   - reality 密钥 / hy2 obfs / 端口 / proxy_path 等**不随 IP 变**（不动），只改 IP 相关项，
#     从而 IP 变更不会让既有用户的密钥失效（仅 sslip 域名与出口 IP 变）。
#   - 全链路幂等：任一步失败都不覆盖 HOST_FILE，下一轮 cron 重试整套（cert 已签会跳过、
#     sed 替换已是新值则 no-op、重启幂等），直到全部成功才提交新 host。
#   - 已知取舍（api 零改）：api 的 host-update 会把 package_connection.connectionUrl 里的
#     oldHost 串替换成 newHost，但我们契约 host 是 {ip}:{port}、订阅链接里是 {ip}:443/{ip}.sslip.io，
#     二者不同串 → **已缓存的老用户订阅链接不会被 api 自动改**（新用户/流量同步不受影响）。
#
# 安装：setup-node.sh 拉本脚本并执行一次（install 模式：自拷到 SCRIPT_PATH + 注册 cron + 初始化 host 文件）。

set -uo pipefail

API_BASE="${API_BASE:-https://api.guayangtou.top}"
TOKEN="${HOST_UPDATE_TOKEN:-node-host-update-secret-token}"
AGENT_API_PORT="${AGENT_API_PORT:-10443}"      # api 契约反代端口；节点 host = {ip}:{port}
ACME_EMAIL="${ACME_EMAIL:-niubdaidai@gmail.com}"

HOST_FILE="/etc/node-agent/node-host"          # 存当前契约 host {ip}:{port}
SSL_DIR="/etc/node-agent/ssl"
NODE_ENV="/etc/node-agent/node.env"
AGENT_CFG="/etc/node-agent/config.yaml"
XRAY_CFG="/usr/local/etc/xray/config.json"
LOG_FILE="/var/log/node-host-update.log"
SCRIPT_PATH="/usr/local/bin/node-host-update.sh"
CRON_FILE="/etc/cron.d/node-host-update"

log() { echo "[$(date '+%F %T')] $1" | tee -a "$LOG_FILE"; }
# 取本机公网 IP：优先问 api（返回调用方 IP），失败回落 ifconfig.me/ip.sb
get_ip() {
    local ip
    ip=$(curl -sf --max-time 10 "${API_BASE}/api/public/ip" 2>/dev/null | grep -oP '(?<="data":")[^"]+')
    [ -z "$ip" ] && ip=$(curl -sf4 --max-time 10 ifconfig.me 2>/dev/null)
    [ -z "$ip" ] && ip=$(curl -sf4 --max-time 10 ip.sb 2>/dev/null)
    echo "$ip"
}
IPRE='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'

# ── 安装模式：首次执行、自身不在目标路径 ──
if [ "$0" != "$SCRIPT_PATH" ] && [ ! -f "$SCRIPT_PATH" ]; then
    log "INFO: 安装 node-host-update"
    cp "$0" "$SCRIPT_PATH"; chmod +x "$SCRIPT_PATH"
    export DEBIAN_FRONTEND=noninteractive
    apt-get install -y -qq curl cron ca-certificates >/dev/null 2>&1 || true
    systemctl enable --now cron 2>/dev/null || true
    mkdir -p "$(dirname "$HOST_FILE")"
    IP=$(get_ip)
    [[ "$IP" =~ $IPRE ]] || { log "ERROR: 安装时取 IP 失败/非法: '$IP'"; exit 1; }
    echo "${IP}:${AGENT_API_PORT}" > "$HOST_FILE"
    log "INFO: 初始化 host = ${IP}:${AGENT_API_PORT}"
    echo "11 * * * * root $SCRIPT_PATH" > "$CRON_FILE"; chmod 644 "$CRON_FILE"
    log "INFO: cron 已注册（每小时第 11 分）；安装完成。日志: $LOG_FILE"
    exit 0
fi

# ── 定时模式 ──
IP=$(get_ip)
[[ "$IP" =~ $IPRE ]] || { log "ERROR: 取公网 IP 失败/非法: '$IP'"; exit 1; }
NEW_HOST="${IP}:${AGENT_API_PORT}"
NEW_DOMAIN="${IP}.sslip.io"

if [ ! -f "$HOST_FILE" ]; then
    mkdir -p "$(dirname "$HOST_FILE")"; echo "$NEW_HOST" > "$HOST_FILE"
    log "INFO: HOST_FILE 不存在，已初始化: $NEW_HOST"; exit 0
fi
OLD_HOST=$(cat "$HOST_FILE")
OLD_IP="${OLD_HOST%%:*}"

[ "$NEW_HOST" = "$OLD_HOST" ] && { log "INFO: host 未变化: $NEW_HOST"; exit 0; }
[[ "$OLD_IP" =~ $IPRE ]] || { log "ERROR: HOST_FILE 旧 IP 非法: '$OLD_IP'，停止等人工"; exit 1; }

OLD_DOMAIN="${OLD_IP}.sslip.io"
log "INFO: host 变更 $OLD_HOST -> $NEW_HOST，开始本地重配（域 $OLD_DOMAIN -> $NEW_DOMAIN）"

# 1) 重签 {新ip}.sslip.io 证书（ZeroSSL 主，LE 兜底；standalone 占用 :80）
ACME="$HOME/.acme.sh/acme.sh"
if [ -x "$ACME" ]; then
    fuser -k 80/tcp >/dev/null 2>&1 || true
    if [ ! -f "$HOME/.acme.sh/${NEW_DOMAIN}_ecc/fullchain.cer" ]; then
        "$ACME" --issue -d "$NEW_DOMAIN" --standalone --keylength ec-256 --server zerossl \
          || "$ACME" --issue -d "$NEW_DOMAIN" --standalone --keylength ec-256 --server letsencrypt \
          || { log "ERROR: 新域证书签发失败，保留旧 host 待下轮重试"; exit 1; }
    fi
    "$ACME" --install-cert -d "$NEW_DOMAIN" --ecc \
        --key-file "$SSL_DIR/key.pem" --fullchain-file "$SSL_DIR/fullchain.pem" \
        --reloadcmd "systemctl reload nginx" >/dev/null 2>&1 || true
else
    log "WARN: 未找到 acme.sh，跳过证书重签（nginx 将用旧证书，浏览器校验会失败）"
fi
[ -s "$SSL_DIR/fullchain.pem" ] || { log "ERROR: 证书文件缺失，回滚等待重试"; exit 1; }

# 2) nginx：decoy + agent-api 两个 server_name 改新 sslip
sed -i "s/${OLD_DOMAIN}/${NEW_DOMAIN}/g" \
    /etc/nginx/sites-available/decoy /etc/nginx/sites-available/agent-api 2>/dev/null || true
if nginx -t >/dev/null 2>&1; then systemctl reload nginx; else log "ERROR: nginx -t 失败，回滚等待重试"; exit 1; fi

# 3) Xray：reality serverNames 改新 sslip（dest=127.0.0.1:8443 不变），重启（agent reconciler 会重灌用户）
[ -f "$XRAY_CFG" ] && sed -i "s/${OLD_DOMAIN}/${NEW_DOMAIN}/g" "$XRAY_CFG" && systemctl restart xray

# 4) agent：public_ip + decoy_domain 改新值，重启（/auto/ 链接随之指向新 IP/sni）
[ -f "$NODE_ENV" ]  && sed -i "s/\b${OLD_IP}\b/${IP}/g; s/${OLD_DOMAIN}/${NEW_DOMAIN}/g" "$NODE_ENV"
[ -f "$AGENT_CFG" ] && sed -i "s/${OLD_IP}/${IP}/g; s/${OLD_DOMAIN}/${NEW_DOMAIN}/g" "$AGENT_CFG"
systemctl restart node-agent 2>/dev/null || true

# 5) 上报 api（按 {ip}:{port} 契约 host 匹配并更新 node.host）
RESP=$(curl -sf --max-time 15 -X POST -H "Content-Type: application/json" \
    -d "{\"oldHost\":\"${OLD_HOST}\",\"host\":\"${NEW_HOST}\",\"token\":\"${TOKEN}\"}" \
    "${API_BASE}/api/public/node/host-update" 2>/dev/null)
if echo "$RESP" | grep -q '"code":200'; then
    log "INFO: api 已更新节点 host: $RESP"
else
    log "WARN: api 上报失败/异常（本地已重配，下轮 cron 重试上报）: ${RESP:-<无响应>}"
    exit 1   # 不提交 HOST_FILE，下轮重试（重配步骤幂等）
fi

# 6) 全链路成功，提交新 host（最终 commit point）
echo "$NEW_HOST" > "$HOST_FILE"
log "INFO: host 更新完成 -> $NEW_HOST"
