#!/bin/bash
#=======================================================================
# relay.sh - 两层中转架构一键脚本(广州前端机专用)
#
# 架构: 手机 --VLESS-Reality--> 本机Xray入站 --balancer--> 多台后端3x-ui出口
#       订阅只含本机节点, 后端UUID/公钥/IP永不下发, 后端切换用户无感
#
# 用法:
#   bash install.sh                 # 进入交互菜单
#   bash install.sh core            # 安装/更新 Xray 内核
#   bash install.sh init [-l 端口] [-s SNI] [--ip 公网IP]   # 初始化前端中转
#   bash install.sh add -n 名字 -a 地址 -p 端口 -u UUID [-T reality|vmess] [-k 公钥] [-i shortId] [-S SNI] [-w flow]
#                      (vmess 后端可加 -H 伪装Host -P 伪装路径, 链接现有免流节点零改动)
#   bash install.sh free [-p 端口] [-H 伪装Host] [-P 路径] [-n 名称]  # 开启免流入口 VMess+HTTP伪装
#   bash install.sh free off        # 关闭免流入口, 订阅切回 Reality 节点
#   bash install.sh del 名字        # 删除后端
#   bash install.sh list            # 后端列表+连通性
#   bash install.sh strategy leastPing|leastLoad|roundRobin|random
#   bash install.sh regen           # 重新生成订阅文件
#   bash install.sh token           # 重置订阅Token
#   bash install.sh status|log|uninstall
#
# 镜像源: 海外 https://ipan.mbvpn.cn/relay   大陆 http://8.134.160.125/relay
#         可用环境变量 RELAY_MIRROR 强制覆盖, 如 RELAY_MIRROR=http://8.134.160.125/relay
#=======================================================================

RELAY_HOME=${RELAY_HOME:-/etc/relay}
BIN_DIR="$RELAY_HOME/bin"
CONF_DIR="$RELAY_HOME/backends.d"
SUB_DIR="$RELAY_HOME/sub"
MAIN_CONF="$RELAY_HOME/main.conf"
CONFIG_JSON="$RELAY_HOME/config.json"
XRAY_BIN="$BIN_DIR/xray"
JQ_BIN="$BIN_DIR/jq"
NGINX_CONF="/etc/nginx/conf.d/relay-sub.conf"
SERVICE_NAME="relay"
SUB_PORT_DEFAULT=8181

IGZ_BASE="http://8.134.160.125/relay"
IPAN_BASE="https://ipan.mbvpn.cn/relay"

# 与镜像内 zip 一一对应, 更新内核包时必须同步更新以下 sha256
SHA_XRAY_AMD64="8195d909f1109b8f3d99eefe401a3c451d7bf4af71f24d3815420f77e5dd2a40"
SHA_XRAY_ARM64="f5698bb218ada3b4022db26fafc39601c5f53b46b19eb76c9616325985807501"
SHA_JQ_AMD64="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
SHA_JQ_ARM64="8b85c817833814ddca00a144c33705546355afccf0cf39b188f3cdb48b852309"

C_RED='\033[0;31m'
C_GREEN='\033[0;32m'
C_YELLOW='\033[0;33m'
C_BLUE='\033[0;34m'
CPlain='\033[0m'

# HTTP 伪装 UA (与常见免流模板一致)
OBFS_UA='Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36'

red()     { echo -e "${C_RED}$*${CPlain}" >&2; }
green()   { echo -e "${C_GREEN}$*${CPlain}"; }
yellow()  { echo -e "${C_YELLOW}$*${CPlain}"; }
blue()    { echo -e "${C_BLUE}$*${CPlain}"; }

have_systemd() {
    [[ $RELAY_NO_SYSTEMD == 1 ]] && return 1
    [[ -d /run/systemd/system ]]
}

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

get_arch() {
    case "$(uname -m)" in
        x86_64|amd64)   echo "amd64" ;;
        aarch64|arm64)  echo "arm64" ;;
        *) red "不支持的架构: $(uname -m)"; return 1 ;;
    esac
}

detect_region() {
    local code=""
    if cmd_exists curl; then
        code=$(curl -sm 8 "http://ip-api.com/json/?fields=countryCode" 2>/dev/null | grep -oP '"countryCode"\s*:\s*"\K[^"]+')
    fi
    [[ -z $code ]] && code="US"
    echo "$code"
}

public_ip() {
    local ip=""
    for u in "http://ip-api.com/json/?fields=query" "https://api.ipify.org"; do
        ip=$(curl -sm 8 "$u" 2>/dev/null | grep -oP '"query"\s*:\s*"\K[^"]+' )
        [[ -z $ip ]] && ip=$(curl -sm 8 "$u" 2>/dev/null)
        [[ -n $ip ]] && { echo "$ip"; return; }
    done
    echo ""
}

mirror_base() {
    if [[ -n $RELAY_MIRROR ]]; then
        echo "${RELAY_MIRROR%/}"
        return
    fi
    local region
    region=$(detect_region)
    if [[ $region == "CN" ]]; then
        echo "$IGZ_BASE $IPAN_BASE"
    else
        echo "$IPAN_BASE $IGZ_BASE"
    fi
}

fetch_file() { # fetch_file <url> <输出文件>
    local url="$1" out="$2"
    rm -f "$out"
    if cmd_exists curl; then
        curl -fL --progress-bar --connect-timeout 10 -o "$out" "$url" && return 0
    fi
    if cmd_exists wget; then
        wget -q --timeout=15 -O "$out" "$url" && return 0
    fi
    return 1
}

fetch_first() { # fetch_first <路径> <输出文件> : 依次尝试各镜像
    local path="$1" out="$2" base
    for base in $(mirror_base); do
        yellow "下载 ${base}/${path}"
        if fetch_file "${base}/${path}" "$out"; then
            green "下载完成: $(ls -lh "$out" | awk '{print $5}')"
            return 0
        fi
        red "下载失败, 尝试下一镜像..."
    done
    red "全部镜像均下载失败: ${path}"
    return 1
}

ensure_deps() {
    if ! cmd_exists curl && ! cmd_exists wget; then
        if cmd_exists apt-get; then
            DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1
            DEBIAN_FRONTEND=noninteractive apt-get install -y curl >/dev/null 2>&1
        elif cmd_exists dnf; then dnf install -y curl >/dev/null 2>&1
        elif cmd_exists yum; then yum install -y curl >/dev/null 2>&1
        elif cmd_exists apk; then apk add curl >/dev/null 2>&1
        fi
    fi
    if ! cmd_exists unzip || ! cmd_exists openssl; then
        if cmd_exists apt-get; then
            DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1
            DEBIAN_FRONTEND=noninteractive apt-get install -y unzip openssl >/dev/null 2>&1
        elif cmd_exists dnf; then dnf install -y unzip openssl >/dev/null 2>&1
        elif cmd_exists yum; then yum install -y unzip openssl >/dev/null 2>&1
        elif cmd_exists apk; then apk add unzip openssl >/dev/null 2>&1
        fi
    fi
    if ! cmd_exists curl && ! cmd_exists wget; then red "缺少 curl/wget 且自动安装失败"; exit 1; fi
    if ! cmd_exists unzip || ! cmd_exists openssl; then red "缺少 unzip/openssl 且自动安装失败"; exit 1; fi
}

port_in_use() { # port_in_use <端口>
    if cmd_exists ss; then
        ss -tln 2>/dev/null | awk '{print $4}' | grep -qE ":$1$"
    else
        (timeout 1 bash -c "</dev/tcp/127.0.0.1/$1") 2>/dev/null
    fi
}

tcp_alive() { # tcp_alive <地址> <端口>
    (timeout 3 bash -c "</dev/tcp/$1/$2") 2>/dev/null
}

#-----------------------------------------------------------------------
# main.conf / backends.d 读写
#-----------------------------------------------------------------------

read_main() { # read_main <KEY>
    [[ -f $MAIN_CONF ]] || return 1
    sed -n "s/^${1}='\\(.*\\)'$/\\1/p" "$MAIN_CONF" | head -1
}

write_main_kv() { # write_main_kv <KEY> <值>
    local key="$1" val="$2"
    touch "$MAIN_CONF"
    if grep -q "^${key}=" "$MAIN_CONF"; then
        sed -i "s|^${key}=.*$|${key}='${val}'|" "$MAIN_CONF"
    else
        echo "${key}='${val}'" >> "$MAIN_CONF"
    fi
}

check_name()   { [[ $1 =~ ^[A-Za-z0-9_-]{1,24}$ ]]; }
check_addr()   { [[ $1 =~ ^[A-Za-z0-9._:-]{1,253}$ ]]; }
check_port()   { [[ $1 =~ ^[0-9]{1,5}$ ]] && (( $1 >= 1 && $1 <= 65535 )); }
check_uuid()   { [[ $1 =~ ^[A-Za-z0-9-]{1,64}$ ]]; }
check_pubkey() { [[ $1 =~ ^[A-Za-z0-9_-]{20,100}$ ]]; }
check_sid()    { [[ -z $1 || $1 =~ ^[A-Za-z0-9]{1,16}$ ]]; }
check_sni()    { [[ $1 =~ ^[A-Za-z0-9.-]{1,253}$ ]]; }

gen_keys() { # 输出: 私钥 公钥
    local out priv pub
    out=$("$XRAY_BIN" x25519 2>/dev/null)
    priv=$(echo "$out" | grep -iE 'private' | head -1 | awk '{print $NF}')
    pub=$(echo "$out" | grep -iE 'public' | head -1 | awk '{print $NF}')
    [[ -n $priv && -n $pub ]] && { echo "$priv $pub"; return 0; }
    return 1
}

gen_uuid()  { cat /proc/sys/kernel/random/uuid 2>/dev/null || "$JQ_BIN" -rn 'uuid'; }
gen_sid()   { openssl rand -hex 4; }
gen_token() { openssl rand -hex 12; }

#-----------------------------------------------------------------------
# 配置生成
#-----------------------------------------------------------------------

build_outbounds() {
    local arr='[]' f obj users otype
    local BK_NAME BK_ADDR BK_PORT BK_UUID BK_PBK BK_SNI BK_SID BK_FP BK_FLOW BK_TYPE BK_OHOST BK_OPATH
    for f in "$CONF_DIR"/*.conf; do
        [[ -f $f ]] || continue
        unset BK_NAME BK_ADDR BK_PORT BK_UUID BK_PBK BK_SNI BK_SID BK_FP BK_FLOW BK_TYPE BK_OHOST BK_OPATH
        # shellcheck disable=SC1090
        source "$f"
        otype=${BK_TYPE:-reality}
        if [[ $otype == "vmess" ]]; then
            obj=$("$JQ_BIN" -cn \
                --arg tag "bk-${BK_NAME}" --arg addr "$BK_ADDR" --argjson port "$BK_PORT" \
                --arg uuid "${BK_UUID}" --arg ohost "${BK_OHOST:-}" --arg opath "${BK_OPATH:-}" \
                --arg ua "$OBFS_UA" '
                { tag:$tag, protocol:"vmess",
                  settings:{ vnext:[ { address:$addr, port:$port,
                    users:[ { id:$uuid, alterId:0, security:"auto" } ] } ] },
                  streamSettings: ({ network:"tcp", security:"none" }
                    + ( if $ohost == "" then {}
                        else { tcpSettings:{ header:{ type:"http",
                          request:{ version:"1.1", method:"GET", path:[$opath],
                            headers:{ Host:[$ohost], "User-Agent":[$ua],
                                      "Accept-Encoding":["gzip,deflate"],
                                      "Connection":["keep-alive"], "Pragma":["no-cache"] } } } } }
                        end ) ) }')
        else
            if [[ -n ${BK_FLOW} ]]; then
                users="[{\"id\":\"${BK_UUID}\",\"encryption\":\"none\",\"flow\":\"${BK_FLOW}\"}]"
            else
                users="[{\"id\":\"${BK_UUID}\",\"encryption\":\"none\"}]"
            fi
            obj=$("$JQ_BIN" -cn \
                --arg tag "bk-${BK_NAME}" --arg addr "$BK_ADDR" --argjson port "$BK_PORT" \
                --argjson users "$users" --arg sni "$BK_SNI" --arg pbk "$BK_PBK" \
                --arg sid "${BK_SID:-}" --arg fp "${BK_FP:-chrome}" '
                { tag:$tag, protocol:"vless",
                  settings:{ vnext:[ { address:$addr, port:$port, users:$users } ] },
                  streamSettings:{ network:"tcp", security:"reality",
                    realitySettings:{ serverName:$sni, fingerprint:$fp,
                                      publicKey:$pbk, shortId:$sid, show:false } } }')
        fi
        arr=$("$JQ_BIN" -cn --argjson a "$arr" --argjson o "$obj" '$a + [$o]')
    done
    printf '%s' "$arr"
}

gen_config_file() {
    local outs strategy uuid port priv sni sid
    local fen fport fuuid fhost fpath
    strategy=$(read_main BALANCE_STRATEGY); strategy=${strategy:-leastPing}
    uuid=$(read_main FRONT_UUID)
    port=$(read_main LISTEN_PORT)
    priv=$(read_main FRONT_PRIVATE_KEY)
    sni=$(read_main FRONT_SNI)
    sid=$(read_main FRONT_SHORT_ID)
    [[ -z $uuid || -z $port || -z $priv ]] && { red "前端未初始化, 先执行 init"; return 1; }
    fen=$(read_main FREE_ENABLED); fen=${fen:-0}
    fport=$(read_main FREE_PORT);  fuuid=$(read_main FREE_UUID)
    fhost=$(read_main FREE_HOST);  fpath=$(read_main FREE_PATH)
    outs=$(build_outbounds)
    if [[ $outs == "[]" ]]; then
        yellow "提示: 尚无后端出口, 先生成黑洞兜底配置(所有入站流量将被拒绝)"
    fi
    "$JQ_BIN" -n \
        --arg uuid "$uuid" --argjson port "$port" \
        --arg priv "$priv" --arg sni "$sni" --arg sid "$sid" \
        --arg strategy "$strategy" --argjson outs "$outs" \
        --argjson fen "${fen:-0}" --argjson fport "${fport:-0}" \
        --arg fuuid "$fuuid" --arg fhost "$fhost" --arg fpath "$fpath" '
    def base_routing: {
      domainStrategy: "AsIs"
    };
    {
      log: { loglevel: "warning" },
      inbounds: [ {
        tag: "relay-in", listen: "0.0.0.0", port: $port, protocol: "vless",
        settings: { clients: [ { id: $uuid, flow: "xtls-rprx-vision" } ], decryption: "none" },
        streamSettings: { network: "tcp", security: "reality",
          realitySettings: { show: false, dest: ($sni + ":443"), xver: 0,
                             serverNames: [$sni], privateKey: $priv, shortIds: [$sid] } },
        sniffing: { enabled: true, destOverride: ["http","tls","quic"] }
      } ] + ( if $fen == 1 then [ {
        tag: "free-in", listen: "0.0.0.0", port: $fport, protocol: "vmess",
        settings: { clients: [ { id: $fuuid, alterId: 0 } ] },
        streamSettings: { network: "tcp", security: "none",
          tcpSettings: { header: { type: "http",
            request: { version: "1.1", method: "GET", path: [$fpath],
              headers: { Host: [$fhost] } } } } },
        sniffing: { enabled: true, destOverride: ["http","tls","quic"] }
      } ] else [] end ),
      outbounds: ( $outs + [ { tag: "direct-out", protocol: "freedom" },
                             { tag: "block-all", protocol: "blackhole" } ] ),
      routing: ( if ($outs | length) == 0 then
          base_routing + { rules: [ { type: "field", outboundTag: "block-all", network: "tcp,udp" } ] }
        else
          base_routing + {
            balancers: [ { tag: "bk-balancer", selector: ["bk-"], strategy: { type: $strategy } } ],
            rules: [ { type: "field", balancerTag: "bk-balancer", network: "tcp,udp" } ]
          }
        end ),
      observatory: {
        subjectSelector: ["bk-"],
        probeUrl: "https://www.gstatic.com/generate_204",
        probeInterval: "30s",
        enableConcurrency: true
      }
    }' > "$CONFIG_JSON.new" || { red "生成配置失败"; return 1; }
    mv "$CONFIG_JSON.new" "$CONFIG_JSON"
}

apply_config() {
    gen_config_file || return 1
    local testout
    testout=$("$XRAY_BIN" run -test -c "$CONFIG_JSON" 2>&1)
    if ! grep -q "Configuration OK" <<<"$testout"; then
        red "配置校验失败:"
        echo "$testout" | tail -5 >&2
        return 1
    fi
    if have_systemd && systemctl cat "$SERVICE_NAME" >/dev/null 2>&1; then
        systemctl restart "$SERVICE_NAME"
        sleep 1
        if systemctl is-active --quiet "$SERVICE_NAME"; then
            green "服务已重启生效"
        else
            red "服务启动失败, 查看日志: journalctl -u ${SERVICE_NAME} -e"
            return 1
        fi
    else
        yellow "配置已生成: $CONFIG_JSON (服务未安装, 跳过重启)"
    fi
}

#-----------------------------------------------------------------------
# 订阅生成
#-----------------------------------------------------------------------

build_vmess_link() { # build_vmess_link <ip> <port> <uuid> <host> <path> <name> -> vmess://...
    local j
    j=$("$JQ_BIN" -cn --arg ps "$6" --arg add "$1" --argjson port "$2" \
             --arg id "$3" --arg path "$5" --arg host "$4" '
          { v:"2", ps:$ps, add:$add, port:$port, id:$id, scy:"auto",
            net:"tcp", tls:"none", type:"http", path:$path, host:$host }') || return 1
    printf 'vmess://%s' "$(printf '%s' "$j" | base64 -w0)"
}

gen_sub_files() {
    local token ip name
    token=$(read_main SUB_TOKEN)
    ip=$(read_main PUBLIC_IP)
    [[ -z $ip ]] && { ip=$(public_ip); [[ -n $ip ]] && write_main_kv PUBLIC_IP "$ip"; }
    [[ -z $ip ]] && { red "无法获取公网IP, 请 init --ip 手动指定"; return 1; }
    mkdir -p "$SUB_DIR"

    # ---- 免流模式: 订阅只含 VMess+HTTP伪装 中转节点 ----
    if [[ $(read_main FREE_ENABLED) == "1" ]]; then
        local fport fuuid fhost fpath fname uri
        fport=$(read_main FREE_PORT);  fuuid=$(read_main FREE_UUID)
        fhost=$(read_main FREE_HOST);  fpath=$(read_main FREE_PATH)
        fname=$(read_main FREE_NAME);  fname=${fname:-Free}
        [[ -z $fport || -z $fuuid || -z $fhost || -z $fpath ]] && { red "免流入站参数不完整"; return 1; }
        name="$fname"
        uri=$(build_vmess_link "$ip" "$fport" "$fuuid" "$fhost" "$fpath" "$name") \
            || { red "生成 vmess 链接失败"; return 1; }
        printf '%s\n' "$uri" | base64 -w0 > "$SUB_DIR/v2ray.txt"
        cat > "$SUB_DIR/clash.yaml" <<EOF
proxies:
  - name: "${name}"
    type: vmess
    server: ${ip}
    port: ${fport}
    udp: true
    uuid: ${fuuid}
    alterId: 0
    cipher: auto
    network: http
    http-opts:
      method: "GET"
      path:
        - "${fpath}"
      headers:
        Host:
          - "${fhost}"
proxy-groups:
  - name: PROXY
    type: select
    proxies:
      - "${name}"
rules:
  - MATCH,PROXY
EOF
        cp -f "$SUB_DIR/clash.yaml" "$SUB_DIR/mihomo.yaml"
        chmod 644 "$SUB_DIR"/*
        green "订阅已生成 (免流模式·只含本机中转节点):"
        blue  "  V2Ray/NekoBox/Happ : http://${ip}:$(read_main SUB_PORT)/sub/${token}/v2ray.txt"
        blue  "  Clash/mihomo       : http://${ip}:$(read_main SUB_PORT)/sub/${token}/clash.yaml"
        return 0
    fi

    # ---- 默认模式: Reality 节点 ----
    local uuid port sni sid pbk
    uuid=$(read_main FRONT_UUID);  port=$(read_main LISTEN_PORT)
    sni=$(read_main FRONT_SNI);    sid=$(read_main FRONT_SHORT_ID)
    pbk=$(read_main FRONT_PUBLIC_KEY)
    [[ -z $uuid || -z $pbk ]] && { red "前端未初始化完整, 无法生成订阅"; return 1; }
    name="Relay-${ip}"

    local uri="vless://${uuid}@${ip}:${port}?type=tcp&security=reality&fp=chrome&sni=${sni}&pbk=${pbk}"
    [[ -n $sid ]] && uri="${uri}&sid=${sid}"
    uri="${uri}&flow=xtls-rprx-vision#${name}"

    printf '%s\n' "$uri" | base64 -w0 > "$SUB_DIR/v2ray.txt"

    cat > "$SUB_DIR/clash.yaml" <<EOF
proxies:
  - name: "${name}"
    type: vless
    server: ${ip}
    port: ${port}
    udp: true
    uuid: ${uuid}
    flow: xtls-rprx-vision
    tls: true
    servername: ${sni}
    client-fingerprint: chrome
    network: tcp
    reality-opts:
      public-key: ${pbk}
      short-id: "${sid}"
proxy-groups:
  - name: PROXY
    type: select
    proxies:
      - "${name}"
rules:
  - MATCH,PROXY
EOF

    cp -f "$SUB_DIR/clash.yaml" "$SUB_DIR/mihomo.yaml"
    chmod 644 "$SUB_DIR"/*
    green "订阅已生成 (只含本机中转节点):"
    blue  "  V2Ray/NekoBox/Happ : http://${ip}:$(read_main SUB_PORT)/sub/${token}/v2ray.txt"
    blue  "  Clash/mihomo       : http://${ip}:$(read_main SUB_PORT)/sub/${token}/clash.yaml"
}

#-----------------------------------------------------------------------
# nginx 订阅站点
#-----------------------------------------------------------------------

setup_nginx_conf() {
    if [[ $RELAY_NO_NGINX == 1 ]]; then yellow "[测试模式] 跳过 nginx 配置"; return 0; fi
    local token port conf_ok=0
    token=$(read_main SUB_TOKEN)
    port=$(read_main SUB_PORT)
    mkdir -p "$SUB_DIR"
    cat > /tmp/relay-sub.conf <<EOF
server {
    listen ${port};
    server_name _;
    access_log off;
    location ^~ /sub/${token}/ {
        alias ${SUB_DIR}/;
        try_files \$uri =404;
        add_header Cache-Control "no-store";
    }
    location / { return 404; }
}
EOF
    if [[ -d /etc/nginx/conf.d ]]; then
        mv /tmp/relay-sub.conf "$NGINX_CONF"; conf_ok=1
    elif [[ -d /etc/nginx/sites-enabled ]]; then
        NGINX_CONF="/etc/nginx/sites-available/relay-sub.conf"
        mv /tmp/relay-sub.conf "$NGINX_CONF"
        ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/relay-sub.conf; conf_ok=1
    fi
    if [[ $conf_ok == 1 ]] && cmd_exists nginx; then
        if nginx -t >/dev/null 2>&1; then
            systemctl reload nginx 2>/dev/null || nginx -s reload 2>/dev/null
            green "nginx 订阅站点已挂载 (端口 ${port})"
        else
            red "nginx -t 校验失败, 已回滚订阅站点配置"
            rm -f "$NGINX_CONF" /etc/nginx/sites-enabled/relay-sub.conf
        fi
    else
        yellow "未检测到 nginx, 订阅文件仅存于 ${SUB_DIR}, 请自行通过 web 服务发布"
        rm -f /tmp/relay-sub.conf
    fi
}

#-----------------------------------------------------------------------
# 子命令
#-----------------------------------------------------------------------

cmd_core() {
    ensure_deps
    mkdir -p "$BIN_DIR" "$CONF_DIR" "$SUB_DIR"
    local arch pkg_arch tmp="/tmp/relay-dl.$$"
    arch=$(get_arch) || return 1
    if [[ $arch == amd64 ]]; then pkg_arch="64"; else pkg_arch="arm64-v8a"; fi
    local zipname="xray-linux-${pkg_arch}.zip" jqname="jq-linux-${arch}"
    local want_xray want_jq
    if [[ $arch == amd64 ]]; then want_xray=$SHA_XRAY_AMD64; want_jq=$SHA_JQ_AMD64
    else want_xray=$SHA_XRAY_ARM64; want_jq=$SHA_JQ_ARM64; fi

    mkdir -p "$tmp"
    fetch_first "xray/${zipname}" "$tmp/core.zip" || { rm -rf "$tmp"; return 1; }
    echo "$want_xray  $tmp/core.zip" | sha256sum -c --quiet >/dev/null 2>&1 \
        || { red "xray 内核 sha256 校验失败"; rm -rf "$tmp"; return 1; }
    unzip -oq "$tmp/core.zip" xray -d "$tmp/x"
    mv -f "$tmp/x/xray" "$XRAY_BIN" && chmod +x "$XRAY_BIN"

    if [[ ! -x $JQ_BIN ]]; then
        fetch_first "xray/${jqname}" "$tmp/jq" && \
        { echo "$want_jq  $tmp/jq" | sha256sum -c --quiet >/dev/null 2>&1 \
            && chmod +x "$tmp/jq" && mv -f "$tmp/jq" "$JQ_BIN" \
            || red "jq sha256 校验失败, 跳过安装"; }
    fi
    rm -rf "$tmp"
    green "Xray 内核版本: $($XRAY_BIN version | head -1)"
}

cmd_init() {
    local listen_port="" sni="" myip=""
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -l) listen_port="$2"; shift 2 ;;
            -s) sni="$2"; shift 2 ;;
            --ip) myip="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    [[ -x $XRAY_BIN ]] || cmd_core || return 1
    mkdir -p "$RELAY_HOME" "$CONF_DIR" "$SUB_DIR"

    if grep -q "^FRONT_UUID=" "$MAIN_CONF" 2>/dev/null; then
        yellow "前端已初始化过, 重复执行将重置全部前端参数与订阅!"
        read -r -p "确认继续? [y/N]: " ans
        [[ ${ans,,} == y ]] || return 0
    fi

    while : ; do
        [[ -n $listen_port ]] || { read -r -p "前端监听端口 [443]: " listen_port; listen_port=${listen_port:-443}; }
        check_port "$listen_port" || { red "端口非法"; listen_port=""; continue; }
        if port_in_use "$listen_port"; then
            red "端口 ${listen_port} 已被占用, 请更换端口"
            listen_port=""
            continue
        fi
        break
    done
    [[ -n $sni ]] || { read -r -p "Reality 伪装 SNI [www.apple.com]: " sni; sni=${sni:-www.apple.com}; }
    check_sni "$sni" || { red "SNI 非法"; return 1; }

    local keys priv pub
    keys=$(gen_keys) || { red "生成 x25519 密钥失败"; return 1; }
    priv=${keys%% *}; pub=${keys##* }

    cat > "$MAIN_CONF" <<EOF
LISTEN_PORT='${listen_port}'
FRONT_UUID='$(gen_uuid)'
FRONT_PRIVATE_KEY='${priv}'
FRONT_PUBLIC_KEY='${pub}'
FRONT_SNI='${sni}'
FRONT_SHORT_ID='$(gen_sid)'
BALANCE_STRATEGY='leastPing'
SUB_TOKEN='$(gen_token)'
SUB_PORT='${SUB_PORT_DEFAULT}'
PUBLIC_IP='${myip:-$(public_ip)}'
EOF
    chmod 600 "$MAIN_CONF"
    apply_config || return 1
    install_service
    setup_nginx_conf
    gen_sub_files
    print_summary
}

install_service() {
    have_systemd || { yellow "无 systemd 环境, 跳过服务安装"; return 0; }
    cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=Relay Xray Service (two-tier relay frontend)
After=network.target nss-lookup.target

[Service]
User=root
ExecStart=${XRAY_BIN} run -c ${CONFIG_JSON}
Restart=on-failure
RestartPreventExitStatus=23
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF
    systemctl daemon-reload
    systemctl enable "$SERVICE_NAME" >/dev/null 2>&1
    systemctl restart "$SERVICE_NAME" && green "systemd 服务 ${SERVICE_NAME}.service 已启动并设为开机自启"
}

print_summary() {
    echo "=============================================="
    green " 前端中转初始化完成"
    echo "----------------------------------------------"
    blue " 本机入口节点(即下发给用户的唯一节点):"
    echo "   端口 : $(read_main LISTEN_PORT)   协议 : VLESS-Reality"
    echo "   SNI  : $(read_main FRONT_SNI)"
    if [[ $(read_main FREE_ENABLED) == "1" ]]; then
        echo "   免流 : 已开启 (端口 $(read_main FREE_PORT), 伪装 $(read_main FREE_HOST)$(read_main FREE_PATH))"
    else
        echo "   免流 : 未开启 (free 命令可开启 VMess+HTTP伪装 入站)"
    fi
    echo "----------------------------------------------"
    yellow " 下一步: 执行『添加后端出口』接入至少一台后端 3x-ui 节点"
    yellow "   bash install.sh add -n hk01 -a 后端IP -p 端口 -u UUID -k 公钥 ..."
    echo "=============================================="
}

cmd_add() {
    local name addr port uuid pbk sid="" sni="" fp="chrome" flow="xtls-rprx-vision"
    local otype="reality" ohost="" opath=""
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -n) name="$2"; shift 2 ;;
            -a) addr="$2"; shift 2 ;;
            -p) port="$2"; shift 2 ;;
            -u) uuid="$2"; shift 2 ;;
            -k) pbk="$2"; shift 2 ;;
            -i) sid="$2"; shift 2 ;;
            -S) sni="$2"; shift 2 ;;
            -F) fp="$2"; shift 2 ;;
            -w) flow="$2"; shift 2 ;;
            -T) otype="$2"; shift 2 ;;
            -H) ohost="$2"; shift 2 ;;
            -P) opath="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    [[ $otype == "reality" || $otype == "vmess" ]] || { red "后端类型只支持 reality|vmess"; return 1; }
    [[ -f $MAIN_CONF ]] || { red "先执行 init 初始化前端"; return 1; }
    [[ -x $JQ_BIN ]] || { red "缺少组件, 先执行 core 安装内核"; return 1; }

    if [[ -z $name ]]; then
        local i=1
        while [[ -f "$CONF_DIR/bk${i}.conf" ]]; do ((i++)); done
        read -r -p "后端名称 [bk${i}]: " name; name=${name:-bk${i}}
    fi
    check_name "$name" || { red "名称只能含字母数字下划线连字符, 长度1-24"; return 1; }
    [[ -f "$CONF_DIR/${name}.conf" ]] && { red "后端 ${name} 已存在"; return 1; }

    [[ -z $addr ]] && read -r -p "后端地址(IP/域名): " addr
    check_addr "$addr" || { red "地址非法"; return 1; }
    [[ -z $port ]] && read -r -p "后端端口: " port
    check_port "$port" || { red "端口非法"; return 1; }
    [[ -z $uuid ]] && read -r -p "后端 UUID: " uuid
    check_uuid "$uuid" || { red "UUID 非法"; return 1; }

    if [[ $otype == "vmess" ]]; then
        [[ -z $ohost ]] && { read -r -p "HTTP伪装Host(留空=纯TCP vmess): " ohost; }
        if [[ -n $ohost ]]; then
            check_sni "$ohost" || { red "伪装 Host 非法"; return 1; }
            [[ -z $opath ]] && { read -r -p "HTTP伪装路径 [/]: " opath; opath=${opath:-/}; }
            [[ $opath =~ ^/[A-Za-z0-9._/-]{0,100}$ ]] || { red "伪装路径须以 / 开头"; return 1; }
        else
            opath=""
        fi
        flow=""; pbk=""; sid=""; sni=""
    else
        [[ -z $pbk ]] && read -r -p "Reality 公钥(pbk): " pbk
        check_pubkey "$pbk" || { red "公钥格式非法"; return 1; }
        [[ -z $sid ]] && { read -r -p "shortId(可留空): " sid; }
        check_sid "$sid" || { red "shortId 非法"; return 1; }
        [[ -z $sni ]] && { read -r -p "后端 SNI [www.apple.com]: " sni; sni=${sni:-www.apple.com}; }
        check_sni "$sni" || { red "SNI 非法"; return 1; }
    fi

    cat > "$CONF_DIR/${name}.conf" <<EOF
BK_NAME='${name}'
BK_TYPE='${otype}'
BK_ADDR='${addr}'
BK_PORT='${port}'
BK_UUID='${uuid}'
BK_PBK='${pbk}'
BK_SID='${sid}'
BK_SNI='${sni}'
BK_FP='${fp}'
BK_FLOW='${flow}'
BK_OHOST='${ohost}'
BK_OPATH='${opath}'
EOF
    chmod 600 "$CONF_DIR/${name}.conf"
    apply_config || { rm -f "$CONF_DIR/${name}.conf"; apply_config; return 1; }
    green "后端出口 [${name}](${otype}) -> ${addr}:${port} 已加入负载均衡"
    cmd_list
}

cmd_del() {
    local name="$1"
    [[ -z $name && $# -eq 0 ]] && {
        cmd_list
        read -r -p "输入要删除的后端名称: " name
    }
    [[ -f "$CONF_DIR/${name}.conf" ]] || { red "后端 ${name} 不存在"; return 1; }
    rm -f "$CONF_DIR/${name}.conf"
    apply_config
    green "后端 [${name}] 已移除, 用户订阅链接不变, 刷新订阅即可继续使用"
}

cmd_list() {
    local f total=0
    blue "---- 后端出口列表 ----"
    printf "%-14s %-28s %s\n" "名称" "地址" "TCP连通"
    for f in "$CONF_DIR"/*.conf; do
        [[ -f $f ]] || continue
        unset BK_NAME BK_ADDR BK_PORT
        # shellcheck disable=SC1090
        source "$f"
        local st="✗"
        tcp_alive "$BK_ADDR" "$BK_PORT" && st="✓"
        printf "%-14s %-28s %s\n" "$BK_NAME" "${BK_ADDR}:${BK_PORT}" "$st"
        total=$((total+1))
    done
    [[ $total -eq 0 ]] && yellow "(空) 尚未添加任何后端"
    blue "均衡策略: $(read_main BALANCE_STRATEGY)   服务: $(have_systemd && systemctl is-active "$SERVICE_NAME" 2>/dev/null || echo n/a)"
}

cmd_strategy() {
    local s="$1"
    [[ -z $s ]] && {
        echo "可选策略: leastPing(默认·健康感知故障切换) leastLoad roundRobin(轮询) random(随机)"
        read -r -p "选择策略: " s
    }
    case "$s" in
        leastPing|leastLoad|roundRobin|random) ;;
        *) red "未知策略: $s"; return 1 ;;
    esac
    [[ -f $MAIN_CONF ]] || { red "先执行 init"; return 1; }
    write_main_kv BALANCE_STRATEGY "$s"
    apply_config
    green "均衡策略已切换为 ${s}"
}

cmd_regen() { gen_sub_files; }

cmd_free() {
    [[ -f $MAIN_CONF ]] || { red "先执行 init 初始化前端"; return 1; }
    [[ -x $XRAY_BIN ]] || { red "缺少内核, 先执行 core"; return 1; }
    if [[ $1 == "off" ]]; then
        write_main_kv FREE_ENABLED 0
        apply_config && gen_sub_files || return 1
        green "免流入站已关闭, 订阅已切回 Reality 节点(链接不变)"
        return 0
    fi
    local port="" host="" path="" name=""
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -p) port="$2"; shift 2 ;;
            -H) host="$2"; shift 2 ;;
            -P) path="$2"; shift 2 ;;
            -n) name="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    local cur_port cur_host cur_path cur_name cur_uuid ip
    cur_port=$(read_main FREE_PORT); cur_host=$(read_main FREE_HOST)
    cur_path=$(read_main FREE_PATH); cur_name=$(read_main FREE_NAME)
    cur_uuid=$(read_main FREE_UUID)
    port=${port:-$cur_port}; port=${port:-80}
    host=${host:-$cur_host}; host=${host:-upay.10010.com}
    path=${path:-$cur_path}; path=${path:-/jf_tjjf}
    name=${name:-$cur_name}; name=${name:-Free}
    check_port "$port" || { red "端口非法"; return 1; }
    check_sni "$host" || { red "伪装 Host 非法"; return 1; }
    [[ $path =~ ^/[A-Za-z0-9._/-]{0,100}$ ]] || { red "伪装路径须以 / 开头(可含字母数字._-/ )"; return 1; }
    [[ ${#name} -le 24 ]] || { red "名称过长(≤24字符)"; return 1; }
    case "$name" in *'"'*|*'\'*|*$'\n'*|*$'\r'*) red "名称含非法字符"; return 1 ;; esac
    if [[ $(read_main FREE_ENABLED) != "1" || $port != "$cur_port" ]] && port_in_use "$port"; then
        red "端口 ${port} 已被占用, 请更换: free -p 端口"; return 1
    fi
    [[ -z $cur_uuid ]] && cur_uuid=$(gen_uuid)
    write_main_kv FREE_ENABLED 1
    write_main_kv FREE_PORT "$port"
    write_main_kv FREE_UUID "$cur_uuid"
    write_main_kv FREE_HOST "$host"
    write_main_kv FREE_PATH "$path"
    write_main_kv FREE_NAME "$name"
    apply_config || { write_main_kv FREE_ENABLED 0; apply_config; return 1; }
    gen_sub_files || return 1
    ip=$(read_main PUBLIC_IP)
    echo
    green "免流入站已开启: VMess + TCP + HTTP伪装(手机直连这段即免流特征段)"
    blue  "  入口     : ${ip}:${port}   伪装 ${host}${path}"
    blue  "  节点链接 : $(build_vmess_link "$ip" "$port" "$cur_uuid" "$host" "$path" "$name")"
    yellow "  说明: 订阅已切换为下发此节点; Reality 入站保留作管理员私有通道"
}

cmd_token() {
    [[ -f $MAIN_CONF ]] || { red "先执行 init"; return 1; }
    write_main_kv SUB_TOKEN "$(gen_token)"
    setup_nginx_conf
    gen_sub_files
    green "订阅 Token 已重置, 旧链接立即失效"
}

cmd_status() {
    have_systemd && systemctl status "$SERVICE_NAME" --no-pager -l || yellow "无 systemd"
    echo
    cmd_list
}

cmd_log() {
    if have_systemd; then journalctl -u "$SERVICE_NAME" -n 50 --no-pager -o cat; else tail -50 "$RELAY_HOME/relay.log" 2>/dev/null; fi
}

cmd_uninstall() {
    read -r -p "确认卸载并删除 ${RELAY_HOME} 全部数据? [y/N]: " ans
    [[ ${ans,,} == y ]] || return 0
    have_systemd && { systemctl disable --now "$SERVICE_NAME" >/dev/null 2>&1; rm -f "/etc/systemd/system/${SERVICE_NAME}.service"; systemctl daemon-reload; }
    rm -rf "$RELAY_HOME"
    rm -f /etc/nginx/conf.d/relay-sub.conf /etc/nginx/sites-available/relay-sub.conf /etc/nginx/sites-enabled/relay-sub.conf
    cmd_exists nginx && nginx -t >/dev/null 2>&1 && { systemctl reload nginx 2>/dev/null || nginx -s reload 2>/dev/null; }
    green "卸载完成"
}

menu() {
    clear 2>/dev/null
    echo "======================================================="
    green  " 两层中转架构管理脚本  (前端隐藏后端 · 用户无感切换)"
    echo "-------------------------------------------------------"
    blue   " 1. 安装/更新 Xray 内核"
    blue   " 2. 初始化前端中转 (生成入站+订阅+nginx)"
    blue   " 3. 添加后端出口节点"
    blue   " 4. 删除后端出口节点"
    blue   " 5. 后端列表与健康状态"
    blue   " 6. 手动重新生成订阅文件"
    blue   " 7. 重置订阅 Token (旧链接失效)"
    blue   " 8. 切换负载均衡策略"
    blue   " 9. 服务状态"
    blue   "10. 查看日志"
    blue   "11. 免流入站开关 (VMess+HTTP伪装)"
    red    "12. 卸载"
    echo    " 0. 退出"
    echo "======================================================="
    read -r -p "请选择: " choice
    case "$choice" in
        1) cmd_core ;;
        2) cmd_init ;;
        3) cmd_add ;;
        4) cmd_del ;;
        5) cmd_list ;;
        6) cmd_regen ;;
        7) cmd_token ;;
        8) cmd_strategy ;;
        9) cmd_status ;;
        10) cmd_log ;;
        11) cmd_free ;;
        12) cmd_uninstall ;;
        0) exit 0 ;;
        *) yellow "无效选项" ;;
    esac
}

case "${1:-}" in
    core)       shift; cmd_core "$@" ;;
    init)       shift; cmd_init "$@" ;;
    add)        shift; cmd_add "$@" ;;
    del|remove) shift; cmd_del "$@" ;;
    list|ls)    cmd_list ;;
    strategy)   shift; cmd_strategy "$@" ;;
    regen)      cmd_regen ;;
    free)       shift; cmd_free "$@" ;;
    token)      cmd_token ;;
    status)     cmd_status ;;
    log|logs)   cmd_log ;;
    uninstall)  cmd_uninstall ;;
    "")         while true; do menu; read -r -p "按回车返回菜单..." _; done ;;
    *)          red "用法: bash $0 [core|init|add|del|free|list|strategy|regen|token|status|log|uninstall]"; exit 1 ;;
esac
