#!/bin/bash
# =====================================================================
# ap5g —— Intel CNVi 无线网卡 (8086:7a70) 5GHz 热点一键管理
#
#   sudo ap5g up [信道]        启动热点（信道可选：149/153/157/161，默认取配置）
#   sudo ap5g down             停止热点并把网卡还给 NetworkManager
#   sudo ap5g restart          重启热点
#   sudo ap5g status           查看热点状态 / 已连设备
#   sudo ap5g channels         列出当前可用的非 DFS 5GHz 信道
#   sudo ap5g set ssid 名字    把配置写进 /etc/ap5g.conf（下次起效）
#   sudo ap5g set pass 密码
#   sudo ap5g set channel 153
#   sudo ap5g show             显示当前配置（密码打码）
#   sudo ap5g repeat start     中继模式：一边连 Wi-Fi 一边发热点
#   sudo ap5g repeat stop
#   sudo ap5g coexist on|off   热点与 Wi-Fi 客户端共存（热点跑虚拟接口 ap0，
#                              信道自动跟随电脑当前连的 Wi-Fi）
#   sudo ap5g coexist status
#   sudo ap5g autostart on|off 开机自动起热点
#   sudo ap5g reload           只重载无线驱动 + 重设 CN 管控域（不动热点）
#   sudo ap5g diag             排障：抓 errno / 2.4G 对照
#   sudo ap5g repair           自救：网卡被误删 / NM 配置被写坏 / 规则残留
#   sudo ap5g rebuild          内核升级后重编译并安装驱动
#
# 配置文件：/etc/ap5g.conf（ap5g set 写入，也可手工编辑）
# =====================================================================
set -uo pipefail

PROG="$(basename "$0")"
# 资产目录自动定位：安装版在 /usr/lib/ap5g，开发期可在 ~/apbuild
SELF="$(readlink -f "$0" 2>/dev/null || echo "$0")"
SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)"
DIR="${AP5G_LIBDIR:-$SELF_DIR}"
[ -f "$DIR/ap-up.sh" ] || DIR="/usr/lib/ap5g"
[ -f "$DIR/ap-up.sh" ] || DIR="/home/zmqsybzc/apbuild"
CONF="${AP5G_CONF:-/etc/ap5g.conf}"
# 运行时状态目录（/run 的小文件：当前 RUNTIME 路径 / pidfile）。
# 做成可覆盖是为了让回归测试不碰宿主机的 /run（沙箱/CI 里它常常不可写）。
RUN_DIR="${AP5G_RUN_DIR:-/run}"

# --- 默认配置 ---
DEF_SSID="AX211-5G"
DEF_PASS="ChangeMe1234"
DEF_CHANNEL="149"
DEF_AP_IF=""
DEF_WAN_IF=""
# 共存模式下的虚拟 AP 接口名；留空 = 独占模式（热点直接占用物理卡，
# 此时网卡不能同时当 Wi-Fi 客户端用）。
DEF_AP_VIF=""
# 换 Wi-Fi 后是否自动把热点搬到新信道（NetworkManager dispatcher 钩子）
DEF_AUTO_FOLLOW="1"

# --- 读配置 ---
# 先记下命令行带进来的环境变量，它们优先级最高（兼容旧的 SSID=x ap-up.sh 用法）
_E_SSID="${SSID:-}"; _E_PASS="${PASS:-}"; _E_CHANNEL="${CHANNEL:-}"
_E_APIF="${AP_IF:-}"; _E_WANIF="${WAN_IF:-}"; _E_APVIF="${AP_VIF:-}"

if [ -r "$CONF" ]; then
    # shellcheck disable=SC1090
    . "$CONF"
fi
SSID="${SSID:-$DEF_SSID}"
PASS="${PASS:-$DEF_PASS}"
CHANNEL="${CHANNEL:-$DEF_CHANNEL}"
AP_IF="${AP_IF:-$DEF_AP_IF}"
WAN_IF="${WAN_IF:-$DEF_WAN_IF}"
AP_VIF="${AP_VIF:-$DEF_AP_VIF}"
AUTO_FOLLOW="${AUTO_FOLLOW:-$DEF_AUTO_FOLLOW}"

[ -n "$_E_SSID" ]    && SSID="$_E_SSID"
[ -n "$_E_PASS" ]    && PASS="$_E_PASS"
[ -n "$_E_CHANNEL" ] && CHANNEL="$_E_CHANNEL"
[ -n "$_E_APIF" ]    && AP_IF="$_E_APIF"
[ -n "$_E_WANIF" ]   && WAN_IF="$_E_WANIF"
[ -n "$_E_APVIF" ]   && AP_VIF="$_E_APVIF"
# 配置里可能被手工写成 off/none/0 表示关闭共存
case "$AP_VIF" in off|none|no|0|false|disabled) AP_VIF="" ;; esac

# --- 接口自动探测 ---
# 要选的是「第一张**物理**无线网卡」。
#
# ⚠ 判据一：不能只看「有 /phy80211 或 /wireless」。nl80211 在 PCI 无线卡上建的
#   虚拟接口（ap0 / p2p0 …）同样有 phy80211。宿主机上残留一个 ap0（旧实验留下的）
#   时，`ls /sys/class/net` 里 ap0 排在 wlp* 前面 → AP_IF 被选成 ap0 →
#   coexist 会把自己判成「与物理卡同名」而拒绝开启，up 也会去操作错的口。
#
# ⚠ 判据二：也不能只看「有 /device」。虚拟口挂在同一张 PCI 设备下，
#   /sys/class/net/ap0/device 一样能解析到那块卡上，所以「有 device」并不等于
#   真硬件。真正可靠的区分是**名字前缀**（systemd 可预测命名 wl*/wlan*）。
#
# 于是按「越像物理卡越优先」排：
#   ① 有 /device 且名字 wl*/wlan*          ← 物理卡的标准形态
#   ② 有 /device 的任意无线口，排除 ap*/p2p* ← 名字不常规的物理卡
#   ③ 任意 wl*/wlan* 无线口                 ← USB 桥接/驱动怪异时可能没有 /device
#   ④ iw dev 兜底
# 所有路径都走 ${SYSFS:-/sys}，便于用伪 sysfs 做回归测试。
detect_ap_if() {
    local S="${SYSFS:-/sys}" d n
    local skip="${AP_VIF:-}"          # 配置里的虚拟口名，绝不能选成物理卡
    # ① 有 /device + 可预测无线名（物理卡的标准形态）
    for d in "$S"/class/net/wl* "$S"/class/net/wlan*; do
        [ -e "$d" ] || continue
        [ -e "$d/device" ] || continue
        [ -d "$d/wireless" ] || [ -d "$d/phy80211" ] || continue
        n="$(basename "$d")"
        [ -n "$skip" ] && [ "$n" = "$skip" ] && continue
        echo "$n"; return 0
    done
    # ② 有 /device 的任意无线口，排除已知虚拟前缀（名字不常规的物理卡）
    for d in "$S"/class/net/*; do
        [ -e "$d" ] || continue
        [ -e "$d/device" ] || continue
        [ -d "$d/wireless" ] || [ -d "$d/phy80211" ] || continue
        n="$(basename "$d")"
        [ -n "$skip" ] && [ "$n" = "$skip" ] && continue
        case "$n" in ap*|p2p*) continue ;; esac
        echo "$n"; return 0
    done
    # ③ 任意 wl*/wlan* 无线口（USB 桥接/驱动怪异时可能没有 /device）
    for d in "$S"/class/net/wl* "$S"/class/net/wlan*; do
        [ -e "$d" ] || continue
        [ -d "$d/wireless" ] || [ -d "$d/phy80211" ] || continue
        n="$(basename "$d")"
        [ -n "$skip" ] && [ "$n" = "$skip" ] && continue
        echo "$n"; return 0
    done
    # ④ 兜底：交给驱动自己说
    iw dev 2>/dev/null | awk '/^\t*Interface/{print $2; exit}'
}
detect_wan_if() {
    ip route get 1.1.1.1 2>/dev/null | sed -n 's/.*dev \([^ ]*\).*/\1/p' | head -1
}
[ -n "$AP_IF" ]  || AP_IF="$(detect_ap_if)"
[ -n "$WAN_IF" ] || WAN_IF="$(detect_wan_if)"
[ -n "$AP_IF" ]  || { echo "错误: 找不到无线接口，请 ap5g set apif <名字>"; exit 1; }
[ -n "$WAN_IF" ] || WAN_IF="eno1"

# 【不要再 export AP_IF / WAN_IF】
# 曾经这里写着 `export AP_IF WAN_IF`，本意是让子脚本少传两个参数。
# 结果 ap-repeater.sh 里有个**同名**变量表示"虚拟 AP 接口名"(默认 ap0)，
# 被这个导出的物理口名覆盖 → 中继脚本把真网卡当残留虚拟口删掉了。
# 现在 ap-up.sh / ap-down.sh 都靠命令行显式传参，无需导出。
# 另外 cmd_repeat 也会用 env -u 主动剥掉这两个变量，双保险。

c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yel='\033[1;33m'
c_red='\033[1;31m'; c_dim='\033[2m'; c_off='\033[0m'
say()  { printf '%b\n' "$*"; }
title(){ printf '%b\n' "${c_blue}==>${c_off} $*"; }
ok()   { printf '%b\n' "  ${c_green}✓${c_off} $*"; }
warn() { printf '%b\n' "  ${c_yel}!${c_off} $*"; }
bad()  { printf '%b\n' "  ${c_red}✗${c_off} $*"; }

require_root() {
    [ "$(id -u)" = 0 ] || { echo "错误: 请用 sudo 运行"; exit 1; }
}

# ap-up.sh 依靠 RUNTIME 区分自己与 libvirt 的 dnsmasq，不能省略
runtime_dir() {
    cat "$RUN_DIR/ap-up.lastrun" 2>/dev/null || echo "$RUN_DIR/ap-up"
}

need_file() {
    [ -f "$1" ] || { bad "缺少文件 $1"; exit 1; }
    [ -x "$1" ] || chmod +x "$1" 2>/dev/null || true
}

# --- 虚拟接口公共库（共存模式：探测客户端信道 / 清理虚拟口）---
# 缺文件时退到内联实现，老的手工拷贝部署也能跑。
if [ -r "$DIR/vif.sh" ]; then
    # shellcheck source=/dev/null
    . "$DIR/vif.sh"
fi
if ! command -v ap_client_channel >/dev/null 2>&1; then
    ap_client_channel() {
        local i
        i="$(iw dev "${1:-}" info 2>/dev/null || true)"
        case "$i" in *"type managed"*) ;; *) return 0 ;; esac
        case "$i" in *ssid*) ;; *) return 0 ;; esac
        printf '%s\n' "$i" | sed -n 's/^[[:space:]]*channel \([0-9]\{1,\}\) (.*/\1/p' | head -1
    }
fi
if ! command -v vif_remove >/dev/null 2>&1; then
    vif_remove() { return 1; }
fi

# 往 /etc/ap5g.conf 写一个键（覆盖同名键；值里可能有空格，所以整行替换）。
# 644：图形界面以普通用户身份读取 SSID/信道来填充表单，权限太紧会读不到。
# 代价是本机其他用户也能看到热点密码 —— 个人桌面可接受，介意就 chmod 600
# 并改用 `ap5g show`（需要 root）查看。
_set_conf() {
    local k="$1" v="$2" tmp
    touch "$CONF"; chmod 644 "$CONF"
    tmp="$(mktemp)"
    grep -vE "^${k}=" "$CONF" 2>/dev/null > "$tmp"
    printf '%s=%s\n' "$k" "$v" >> "$tmp"
    mv "$tmp" "$CONF"; chmod 644 "$CONF"
}

# 网卡是否上报了「同一个组合里既有 managed 又有 AP」——这是共存模式的硬件前提。
# 只看单行（Intel 这张卡就是单行的 #{ managed } …, #{ AP, P2P-client, P2P-GO } …）。
# 判定失败只当警告、不当拒绝：组合换行时会误判，不值得为此挡住用户。
coexist_supported() {
    iw phy 2>/dev/null | grep -E '\* *#\{ managed \}.*#\{ AP' >/dev/null 2>&1
}

# ------------------------------------------------------------------
cmd_up() {
    require_root
    [ $# -ge 1 ] && CHANNEL="$1"
    need_file "$DIR/ap-up.sh"

    title "启动 5GHz 热点"
    if [ -n "$AP_VIF" ]; then
        if [ "$AP_VIF" = "$AP_IF" ]; then
            bad "AP_VIF（$AP_VIF）不能与物理无线网卡同名 $AP_IF"
            say "    ${c_dim}修： sudo ap5g coexist on${c_off}"
            exit 1
        fi
        say "  ${c_dim}模式:${c_off} 共存（热点在虚拟接口 $AP_VIF；$AP_IF 仍归 NM 做 Wi-Fi 客户端）"
        local _cc
        _cc="$(ap_client_channel "$AP_IF")"
        if [ -n "$_cc" ]; then
            say "  ${c_dim}客户端:${c_off} $AP_IF 连在信道 $_cc → 热点会跟着用 $_cc（驱动限制同信道）"
        else
            say "  ${c_dim}客户端:${c_off} $AP_IF 当前未连 Wi-Fi（热点用配置信道 $CHANNEL）"
        fi
    else
        say "  ${c_dim}模式:${c_off} 独占（$AP_IF 被热点占用，期间不能当 Wi-Fi 客户端）"
    fi
    say "  SSID=$SSID  信道=$CHANNEL  接口=$AP_IF  出口=$WAN_IF"

    # 起飞前自检
    local self_managed=""
    self_managed="$(iw phy 2>/dev/null | grep -c 'self-managed')"
    if [ "${self_managed:-0}" != "0" ]; then
        warn "wiphy 仍是 self-managed 管控域，5GHz 会被强制 no IR。"
        warn "请先执行: sudo ap5g reload"
    fi
    if ! iw reg get 2>/dev/null | grep -q 'country CN'; then
        warn "主机管控域不是 CN，正在补设…"
        iw reg set CN || true
    fi
    # 事故遗留：旧版本中继脚本曾经把这块**物理**卡写成 NM 的 unmanaged-devices
    # （文件名却是 99-unmanaged-ap0.conf），于是 NM 永不接管它 —— 热点一失败，
    # 网卡就回不到 NM 手里，连普通 Wi-Fi 都用不了。
    if grep -rqs "interface-name:$AP_IF" /etc/NetworkManager/conf.d/ 2>/dev/null; then
        warn "$AP_IF 被 NetworkManager 配成了 unmanaged（旧版本事故遗留）"
        warn "修复： sudo ap5g repair"
    fi

    CHANNEL="$CHANNEL" SSID="$SSID" PASS="$PASS" AP_IF="$AP_IF" WAN_IF="$WAN_IF" \
        AP_VIF="$AP_VIF" "$DIR/ap-up.sh"
}

cmd_down() {
    require_root
    need_file "$DIR/ap-down.sh"
    title "停止热点"
    AP_IF="$AP_IF" WAN_IF="$WAN_IF" AP_VIF="$AP_VIF" "$DIR/ap-down.sh"
    ok "热点已停止，网卡已交还 NetworkManager"
}

cmd_status() {
    title "热点状态"
    local hp=0 dns=0 rt
    rt="$(runtime_dir)"
    if [ -f "$rt/hostapd.pid" ] && kill -0 "$(cat "$rt/hostapd.pid")" 2>/dev/null; then
        hp=1; ok "hostapd 运行中（pid $(cat "$rt/hostapd.pid")）"
    else
        bad "hostapd 未运行"
    fi
    if [ -f "$rt/dnsmasq.pid" ] && kill -0 "$(cat "$rt/dnsmasq.pid")" 2>/dev/null; then
        dns=1; ok "dnsmasq 运行中（pid $(cat "$rt/dnsmasq.pid")）"
    else
        bad "dnsmasq 未运行"
    fi
    echo
    # 真正跑着的 AP 接口以运行时配置为准（共存模式是虚拟口，独占模式是物理卡）
    local hot
    hot="$(sed -n 's/^interface=//p' "$rt/hostapd.conf" 2>/dev/null | head -1)"
    [ -n "$hot" ] || hot="${AP_VIF:-$AP_IF}"
    if [ -n "$AP_VIF" ]; then
        say "  ${c_dim}模式:${c_off} 共存（热点 $hot + Wi-Fi 客户端 $AP_IF）"
    else
        say "  ${c_dim}模式:${c_off} 独占（热点占用 $AP_IF，期间不能当 Wi-Fi 客户端；sudo ap5g coexist on 可改）"
    fi
    say "  ${c_dim}接口:${c_off} $hot    ${c_dim}出口:${c_off} $WAN_IF"
    if [ "$hp" = 1 ]; then
        iw dev "$hot" info 2>/dev/null | sed -n 's/^[[:space:]]*/  /p' | head -8
        echo
        say "  ${c_dim}已连接设备:${c_off}"
        local st
        st="$(iw dev "$hot" station dump 2>/dev/null | grep -E '^Station' | awk '{print "    " $2}')"
        [ -n "$st" ] && say "$st" || say "    （暂无）"
        echo
        local _ch _cw _want
        _ch="$(iw dev "$hot" info 2>/dev/null | sed -n 's/.*channel \([0-9]*\).*/\1/p' | head -1)"
        _cw="$(iw dev "$hot" info 2>/dev/null | sed -n 's/.*width: \([0-9]*\) MHz.*/\1/p' | head -1)"
        say "  ${c_dim}当前信道:${c_off} ${_ch:-?}   ${c_dim}频宽:${c_off} ${_cw:-?} MHz"
        # 频宽静默降级核对：hostapd 在次信道出块时会悄悄退到 20MHz，退出码仍是 0。
        # 只看「起来没有」发现不了，用户只会觉得「能连上但很慢」。
        _want=""
        if [ -r "$DIR/chanplan.sh" ]; then
            _want="$( . "$DIR/chanplan.sh" >/dev/null 2>&1; chanplan_max_width "${_ch:-0}" )"
        fi
        if [ -n "$_cw" ] && [ -n "$_want" ] && [ "$_cw" -lt "$_want" ]; then
            warn "频宽只有 ${_cw}MHz，而信道 ${_ch} 本可到 ${_want}MHz（手机速率会被压到约 1/4）"
            say "     ${c_dim}多半是 hostapd 认为 40MHz 次信道非法而静默降级；改用 149/153/157/161 后 sudo ap5g restart${c_off}"
        fi
        # 共存模式的核心约束：热点与客户端必须同信道（驱动 #channels<=1）。
        if [ -n "$AP_VIF" ]; then
            local _cc
            _cc="$(ap_client_channel "$AP_IF")"
            if [ -n "$_cc" ] && [ -n "$_ch" ] && [ "$_cc" != "$_ch" ]; then
                warn "客户端 $AP_IF 在信道 $_cc，热点却在信道 $_ch —— 两者必须同信道才能同时工作"
                say "     ${c_dim}执行 sudo ap5g restart 让热点重新对齐客户端信道${c_off}"
            elif [ -n "$_cc" ]; then
                ok "客户端 $AP_IF 与热点同在信道 $_cc"
            fi
        fi
    fi
    echo
    say "  ${c_dim}NAT 规则:${c_off}"
    iptables -t nat -S 2>/dev/null | grep -i masquerade | head -3 | sed 's/^/    /' || say "    （无）"
}

cmd_channels() {
    title "当前可用的 5GHz 信道"
    local w
    w="$(iw phy 2>/dev/null | awk '/^Wiphy/{print $2; exit}')"
    [ -n "$w" ] || { bad "找不到 wiphy，无线可能没起来"; exit 1; }
    printf '  %-6s %-32s %s\n' "信道" "状态" "最大宽度"
    # iw 把限制标记写在紧随其后的缩进行里（Radar detection / no IR / disabled ...），
    # 必须攒够一整个块再判定，只扫 "^\t* *" 那行会漏掉全部标记。
    iw phy "$w" channels 2>/dev/null | awk '
        function rec(c, f, s, bw) {
            bw=""
            if (f ~ /VHT160/)      bw="160"
            else if (f ~ /VHT80/)  bw="80"
            else                   bw="20/40"
            s="可用"
            if (f ~ /disabled/)          s="禁用(regdb)"
            else if (f ~ /Radar detection/) s="DFS 需监听雷达，启动慢约1分钟"
            else if (f ~ /[Nn]o IR/)     s="受限 no IR"
            return sprintf("  %-6s %-32s %s", c, s, bw)
        }
        /^Band 2:/ {inb=1; next}
        /^Band [13]:/ {inb=0; next}
        inb && /^[[:space:]]*\*/ {
            if (ch != "") print rec(ch, flags)
            ch=""; flags=$0          # 同一行的 (disabled) / (no IR) 也算标记
            l=$0; gsub(/[][*()]/," ",l); split(l,a," ")
            ch=a[3]
            if (ch !~ /^[0-9]+$/) ch=""
            next
        }
        inb { flags = flags " " $0; next }
        END { if (inb && ch != "") print rec(ch, flags) }'
    echo
    say "  ${c_dim}推荐（CN 域非 DFS、支持 80MHz）：149 / 153 / 157 / 161${c_off}"
    say "  ${c_dim}这 4 个主信道同属 149-161 这个 80MHz 块（中心 5775 / 索引 155），任意一个都要 80MHz 都得让${c_off}"
    say "  ${c_dim}40MHz 次信道落在块内 —— ap-up.sh 已按信道自动推导方向（149/153 取上 → HT40+；157/161 取下 → HT40-）。${c_off}"
    say "  ${c_dim}165 没有 80MHz 块，只能 20/40MHz。${c_off}"
}

cmd_set() {
    require_root
    [ $# -ge 2 ] || { echo "用法: ap5g set <ssid|pass|channel|apif|wanif|vif|autofollow> <值>"; exit 1; }
    # 用户输入用小写简写，配置文件里是大写键
    local k="$1" v="$2"
    case "$(printf '%s' "$k" | tr '[:upper:]' '[:lower:]')" in
        ssid)        k=SSID ;;
        pass|password|passwd) k=PASS ;;
        channel|ch)  k=CHANNEL ;;
        apif|ap_if)  k=AP_IF ;;
        wanif|wan_if) k=WAN_IF ;;
        vif|apvif|ap_vif|coexist) k=AP_VIF ;;
        autofollow|follow|auto) k=AUTO_FOLLOW ;;
        *) echo "用法: ap5g set <ssid|pass|channel|apif|wanif|vif|autofollow> <值>"; exit 1 ;;
    esac
    if [ "$k" = CHANNEL ]; then
        case "$v" in
            ''|*[!0-9]*) echo "错误: 信道必须是数字"; exit 1 ;;
        esac
        case "$v" in
            165) warn "信道 165 没有 80MHz 块（最多 20/40MHz）；推荐 149/153/157/161" ;;
            52|56|60|64|100|104|108|112|116|120|124|128|132|136|140|144)
                warn "信道 $v 属 DFS，需要 ieee80211h=1 且启动时要听约 1 分钟雷达；推荐 149/153/157/161" ;;
            [3-9][0-9]|1[0-6][0-9]) ;;
            *) warn "信道 $v 不在常见 5GHz 列表里，请确认 0 < 信道 <= 165" ;;
        esac
    fi
    if [ "$k" = AP_VIF ]; then
        # off/none/空 都表示关闭共存（回到独占模式）
        case "$v" in
            off|none|no|0|false|disabled) v="" ;;
        esac
        if [ -n "$v" ]; then
            case "$v" in
                [!A-Za-z]*|*[!A-Za-z0-9_-]*)
                    echo "错误: 接口名不合法（$v）；只能字母/数字/_/-，且以字母开头"; exit 1 ;;
            esac
            if [ "$v" = "$AP_IF" ]; then
                echo "错误: 虚拟接口名不能与物理无线网卡同名（$AP_IF）"; exit 1
            fi
            if ! coexist_supported; then
                warn "网卡能力里没找到「客户端 + AP」并存组合，仍会尝试；失败就 coexist off"
            fi
        fi
    fi
    if [ "$k" = AUTO_FOLLOW ]; then
        case "$v" in
            on|yes|true|enable|1)  v=1 ;;
            off|no|false|disable|0) v=0 ;;
        esac
        case "$v" in
            0|1) ;;
            *) echo "错误: 只能写 on / off（或 1 / 0）"; exit 1 ;;
        esac
    fi
    _set_conf "$k" "$v"
    ok "已写入 $CONF：$k=$( [ "$k" = PASS ] && echo '********' || echo "${v:-（空）}" )"
    case "$k" in
        ssid|pass|channel)
            say "  ${c_dim}让改动立即生效：sudo ap5g restart${c_off}" ;;
        AUTO_FOLLOW)
            if [ "$v" = 1 ]; then
                say "  ${c_dim}换 Wi-Fi 后会通过 NetworkManager 钩子自动把热点搬到新信道${c_off}"
            else
                say "  ${c_dim}已关闭自动跟随：换 Wi-Fi 后请手动 sudo ap5g restart${c_off}"
            fi ;;
        AP_VIF)
            if [ -n "$v" ]; then
                say "  ${c_dim}共存模式已开启：热点跑虚拟接口 $v，$AP_IF 继续做 Wi-Fi 客户端${c_off}"
                say "  ${c_dim}两者必须同信道，热点会自动跟随电脑当前连的 Wi-Fi${c_off}"
            else
                say "  ${c_dim}共存模式已关闭：热点将独占 $AP_IF${c_off}"
            fi
            say "  ${c_dim}让改动立即生效：sudo ap5g restart${c_off}" ;;
    esac
}

cmd_show() {
    title "当前配置（$CONF）"
    printf '  SSID      %s\n' "${SSID:-（未设置）}"
    printf '  密码      %s\n' "$( [ -n "$PASS" ] && printf '%.0s*' $(seq 1 ${#PASS}) || echo '（未设置）')"
    printf '  信道      %s\n' "$CHANNEL"
    printf '  AP接口    %s\n' "$AP_IF"
    printf '  出口      %s\n' "$WAN_IF"
    if [ -n "$AP_VIF" ]; then
        printf '  共存      on（热点跑虚拟口 %s；%s 仍作 Wi-Fi 客户端）\n' "$AP_VIF" "$AP_IF"
        printf '  自动跟随  %s%s\n' "${AUTO_FOLLOW:-1}" \
               "$( [ "${AUTO_FOLLOW:-1}" = 0 ] && echo '' || echo '（换 Wi-Fi 后自动搬信道）' )"
    else
        printf '  共存      off（热点独占 %s，期间不能当 Wi-Fi 客户端）\n' "$AP_IF"
    fi
    local autosvc="off"
    systemctl is-enabled ap5g.service >/dev/null 2>&1 && autosvc="on"
    printf '  开机自启  %s\n' "$autosvc"
    echo
    local sm
    sm="$(modinfo iwlmvm 2>/dev/null | grep -c '^parm:.*lar_country_code')"
    if [ "$sm" = 1 ]; then
        ok "驱动补丁已安装（v3：含固件 MCC 下发）"
    else
        warn "当前内核的 iwlmvm 没有 disable_lar 补丁，5GHz 会被锁定"
        say "    执行：sudo ap5g rebuild"
    fi
    iw reg get 2>/dev/null | head -1 | sed 's/^/  /'
}

cmd_repeat() {
    require_root
    need_file "$DIR/ap-repeater.sh"
    local sub="${1:-status}"; shift 2>/dev/null || true
    title "中继模式：$sub"
    # env -u：把 AP_IF（物理口名）彻底挡在中继脚本之外。中继脚本自己用
    # AP5G_VIF 指定虚拟口名（默认 ap0），两者绝不能混。
    env -u AP_IF -u WAN_IF \
        UPSTREAM="$AP_IF" \
        AP5G_VIF="${AP5G_VIF:-ap0}" \
        SSID="$SSID" PASS="$PASS" \
        "$DIR/ap-repeater.sh" "$sub" "$@"
}

cmd_coexist() {
    local act="${1:-status}" vif="${2:-ap0}"
    case "$act" in
        on|enable|1)
            require_root
            title "开启共存模式：热点 + Wi-Fi 客户端同时用"
            if coexist_supported; then
                ok "网卡上报了「客户端 + AP」并存组合（两者共占一条信道）"
            else
                warn "没在网卡能力里找到「客户端 + AP」并存组合，仍会尝试"
                warn "若启动失败，用 sudo ap5g coexist off 回到独占模式"
            fi
            case "$vif" in
                [!A-Za-z]*|*[!A-Za-z0-9_-]*) bad "接口名不合法：$vif（只能字母/数字/_/-，且以字母开头）"; exit 1 ;;
            esac
            if [ "$vif" = "$AP_IF" ]; then
                bad "虚拟接口名不能与物理网卡同名（$AP_IF）"; exit 1
            fi
            _set_conf AP_VIF "$vif"
            ok "已写入 $CONF：AP_VIF=$vif"
            say "  ${c_dim}热点改跑在虚拟接口 $vif 上，$AP_IF 继续由 NetworkManager 当 Wi-Fi 客户端${c_off}"
            say "  ${c_dim}驱动限制两者必须同信道 → 热点会自动跟随电脑当前连的 Wi-Fi 信道${c_off}"
            if [ -z "$(ap_client_channel "$AP_IF")" ]; then
                say "  ${c_dim}提示：现在电脑还没连 Wi-Fi，热点先用配置信道 $CHANNEL；连上后会重对齐${c_off}"
            fi
            say "  ${c_dim}生效： sudo ap5g restart${c_off}"
            ;;
        off|disable|0|none)
            require_root
            title "关闭共存模式（热点独占网卡）"
            local stale="${AP_VIF:-}"
            _set_conf AP_VIF ""
            ok "已写入 $CONF：AP_VIF=（空，独占模式）"
            if [ -n "$stale" ] && ip link show "$stale" >/dev/null 2>&1; then
                if vif_remove "$stale" "$AP_IF" "$WAN_IF"; then
                    ok "已删除残留的虚拟接口 $stale"
                fi
            fi
            say "  ${c_dim}注意：之后 $AP_IF 被热点占用，期间不能当 Wi-Fi 客户端${c_off}"
            say "  ${c_dim}生效： sudo ap5g restart${c_off}"
            ;;
        status|"")
            title "共存模式"
            if [ -n "$AP_VIF" ]; then
                ok "已开启：热点接口 $AP_VIF，物理卡 $AP_IF 作 Wi-Fi 客户端"
                local cc
                cc="$(ap_client_channel "$AP_IF")"
                if [ -n "$cc" ]; then
                    say "  ${c_dim}客户端当前信道：${c_off}$cc（热点会跟着用这个信道）"
                else
                    say "  ${c_dim}客户端当前未连接 Wi-Fi${c_off}"
                fi
                if [ -n "$cc" ]; then
                    case "$cc" in
                        52|56|60|64|100|104|108|112|116|120|124|128|132|136|140|144)
                            warn "客户端在 DFS 信道 $cc：热点无法与它共存（需约 1 分钟雷达检测）"
                            say "     ${c_dim}把电脑 Wi-Fi 改连 36-48 / 149-165 或 2.4GHz${c_off}" ;;
                        [1-9]|1[0-4])
                            warn "客户端在 2.4GHz：热点也只能上 2.4GHz（上限 20MHz，速率低）" ;;
                    esac
                fi
            else
                warn "未开启（独占模式）：$AP_IF 被热点占用，期间没法当 Wi-Fi 客户端用"
                say "  ${c_dim}开启： sudo ap5g coexist on${c_off}"
            fi
            if coexist_supported; then
                ok "网卡支持「客户端 + AP」并存"
            else
                warn "未检测到「客户端 + AP」并存的接口组合"
            fi
            ;;
        *) echo "用法: ap5g coexist [on|off|status] [虚拟接口名，默认 ap0]"; exit 1 ;;
    esac
}

cmd_autostart() {
    require_root
    local sub="${1:-status}"
    case "$sub" in
        on)
            systemctl enable ap5g.service >/dev/null 2>&1 || { bad "enable 失败"; exit 1; }
            ok "已开启开机自启热点" ;;
        off)
            systemctl disable ap5g.service >/dev/null 2>&1 || true
            ok "已关闭开机自启" ;;
        status)
            systemctl is-enabled ap5g.service 2>/dev/null | sed 's/^/  /' ;;
        *) echo "用法: ap5g autostart on|off|status"; exit 1 ;;
    esac
}

cmd_reload() {
    require_root
    need_file "$DIR/reload-wifi.sh"
    title "重载无线驱动并重设 CN 管控域"
    "$DIR/reload-wifi.sh"
}

cmd_rebuild() {
    require_root
    need_file "$DIR/rebuild-driver.sh"
    "$DIR/rebuild-driver.sh" "$@"
}

cmd_diag() {
    require_root
    need_file "$DIR/ap-diag.sh"
    "$DIR/ap-diag.sh" "$@"
}

cmd_probe() {
    # 给图形前端用：一次性输出只读信息，分段标记便于解析。
    # 走进 polkit 策略里放行的那个入口，避免为读几条规则单开 bash 授权。
    require_root
    printf '### NAT\n'
    iptables -t nat -S 2>/dev/null | grep MASQUERADE || true
    printf '### FORWARD\n'
    iptables -S FORWARD 2>/dev/null | grep -E "$AP_IF" || true
    printf '### STATIONS\n'
    iw dev "$AP_IF" station dump 2>/dev/null || true
    printf '### LEASES\n'
    cat /var/lib/misc/dnsmasq.leases 2>/dev/null || true
    printf '### END\n'
}

cmd_repair() {
    require_root
    title "修复无线状态"
    local fixed=0

    # ---- 1) 清掉把物理网卡标成 unmanaged 的错误 NM 配置 ----
    # （中继脚本变量名冲突那次留下的后遗症）
    local nmf=/etc/NetworkManager/conf.d/99-unmanaged-ap0.conf
    if [ -f "$nmf" ]; then
        local bad
        bad="$(sed -n 's/.*interface-name:\(.*\)/\1/p' "$nmf" 2>/dev/null | tr '\n' ' ')"
        if [ -n "$bad" ] && printf '%s\n' $bad | grep -qvE '^ap[0-9]+$'; then
            rm -f "$nmf"
            ok "已删除内容有误的 $nmf（原指向物理口：$bad）"
            fixed=1
        else
            ok "$nmf 内容正常"
        fi
    fi

    # ---- 2) 无线接口不见了 → 重载驱动重建 ----
    local wl phy
    wl="$(detect_ap_if || true)"
    if [ -z "$wl" ]; then
        warn "找不到无线接口，尝试重建 …"
        if iw phy 2>/dev/null | grep -q '^Wiphy'; then
            warn "驱动与 phy 都在，只是 netdev 被删了 —— 重载 iwlwifi"
            modprobe -r iwlmvm iwlwifi 2>/dev/null || true
            sleep 1
            modprobe iwlwifi 2>/dev/null || true
            sleep 3
            wl="$(detect_ap_if || true)"
            if [ -z "$wl" ]; then
                phy="$(iw phy 2>/dev/null | awk '/^Wiphy/{print $2; exit}')"
                if [ -n "$phy" ]; then
                    warn "重载没恢复，直接在 $phy 上把接口加回来"
                    iw phy "$phy" interface add "$AP_IF" type managed 2>/dev/null || true
                    sleep 2
                    wl="$(detect_ap_if || true)"
                fi
            fi
        else
            warn "连 phy 都没有，尝试加载 iwlwifi"
            modprobe iwlwifi 2>/dev/null || true
            sleep 3
            wl="$(detect_ap_if || true)"
        fi
        if [ -n "$wl" ]; then ok "无线接口已恢复：$wl"; fixed=1
        else bad "仍找不到无线接口 —— 查硬件开关 / BIOS / dmesg | grep -i iwl"; fi
    else
        ok "无线接口正常：$wl"
    fi

    # ---- 2.5) 物理卡被误设为 AP 模式 → 复位为 managed（客户端） ----
    # 即使 hostapd 已被手动杀掉，接口类型可能仍停在 AP，NM 无法当普通 Wi-Fi 用。
    SYSFS="${SYSFS:-/sys}"
    if [ -n "$wl" ] && [ -e "$SYSFS/class/net/$wl/phy80211" ] \
       && iw dev "$wl" info 2>/dev/null | grep -q 'type AP'; then
        warn "$wl 仍处于 AP 模式（事故遗留），复位为 managed"
        ip link set "$wl" down 2>/dev/null || true
        iw dev "$wl" set type managed 2>/dev/null || \
            warn "复位类型失败（可手动：sudo iw dev $wl set type managed）"
        fixed=1
    fi

    # ---- 3) 清掉可能残留的热点进程与 NAT/转发规则 ----
    if [ -f "$RUN_DIR/ap-up.lastrun" ] || pgrep -x hostapd >/dev/null 2>&1; then
        need_file "$DIR/ap-down.sh"
        AP_IF="$AP_IF" WAN_IF="$WAN_IF" AP_VIF="$AP_VIF" "$DIR/ap-down.sh" >/dev/null 2>&1 || true
        ok "已清理残留的热点进程 / NAT 规则"
        fixed=1
    fi

    # ---- 3.5) 清掉残留的虚拟 AP 接口 ----
    # 只有「确定没人用」时才动：上一步已经收拾过我们自己的 hostapd，
    # 如果现在系统里一个 hostapd 都没有，那么 ap0 这类 802.11 虚拟口就是残留。
    # 删接口仍然走 vif_remove（会硬拒物理卡 / 上行口 / 非 802.11 接口）。
    if ! pgrep -x hostapd >/dev/null 2>&1; then
        local vv
        for vv in ${AP_VIF:-} ap0; do
            [ -n "$vv" ] || continue
            [ "$vv" = "$AP_IF" ] && continue
            [ "$vv" = "$WAN_IF" ] && continue
            if ip link show "$vv" >/dev/null 2>&1 && vif_remove "$vv" "$AP_IF" "$WAN_IF"; then
                ok "已删除残留虚拟接口 $vv"
                fixed=1
            fi
        done
    fi

    # ---- 4) 管控域 + 交还 NetworkManager ----
    iw reg set CN 2>/dev/null || true
    ok "管控域：$(iw reg get 2>/dev/null | sed -n '2p' | sed 's/^ *//')"
    if systemctl is-active --quiet NetworkManager 2>/dev/null; then
        [ -n "$wl" ] && nmcli dev set "$wl" managed yes 2>/dev/null || true
        nmcli radio wifi on 2>/dev/null || true
        systemctl reload-or-restart NetworkManager 2>/dev/null || true
        ok "NetworkManager 已恢复，稍候会自动回连 Wi-Fi"
    fi

    echo
    if [ "$fixed" = 1 ]; then ok "修复完成"
    else                       ok "未发现需要修复的问题"; fi
}

usage() {
    awk '/^# ap5g ——/{f=1; print substr($0,3); next}
         f{ if ($0 ~ /^# ===/) exit; print substr($0,3) }' "$0"
}

# --- 入口 ---
case "${1:-}" in
    up)         shift; cmd_up "$@" ;;
    down|stop)  cmd_down ;;
    restart)    cmd_down; sleep 2; shift; cmd_up "$@" ;;
    status|st)  cmd_status ;;
    channels|ch) cmd_channels ;;
    set)        shift; cmd_set "$@" ;;
    show|conf)  cmd_show ;;
    repeat)     shift; cmd_repeat "$@" ;;
    coexist)    shift; cmd_coexist "$@" ;;
    autostart)  shift; cmd_autostart "$@" ;;
    reload)     shift; cmd_reload "$@" ;;
    rebuild)    shift; cmd_rebuild "$@" ;;
    diag)       shift; cmd_diag "$@" ;;
    repair)     shift; cmd_repair "$@" ;;
    probe)      cmd_probe ;;
    help|-h|--help) usage ;;
    "")         usage; exit 1 ;;
    *)          echo "未知命令：$1"; usage; exit 1 ;;
esac
