#!/bin/sh
# 3m-ui unified management entrypoint (/usr/local/bin/3m-ui).
# Application binary: /usr/local/lib/3m-ui/3m-ui-bin
# Original work for 3m-ui (EPL-2.0). Ideas inspired by common panel ops menus;
# not a derivative of any third-party management script source.

set -eu

ROOT="${THREE_M_UI_ROOT:-}"
BASE="$ROOT/usr/local/lib/3m-ui"
MANAGER="$BASE/3m-ui.sh"
INSTALLER="$BASE/install.sh"
UPDATER="$BASE/update.sh"
UNINSTALLER="$BASE/uninstall.sh"
APP_BIN="$BASE/3m-ui-bin"
CONFIG_FILE="$ROOT/etc/3m-ui/config.yaml"
DATA_DIR="${THREE_M_UI_DATA_DIR:-}"
if [ -z "$DATA_DIR" ] && [ -s "$BASE/DATA_DIRECTORY" ]; then
    DATA_DIR="$(cat "$BASE/DATA_DIRECTORY")"
fi
DATA_DIR="${DATA_DIR:-$ROOT/var/lib/3m-ui}"

# --- helpers -----------------------------------------------------------------

say() { printf '%s\n' "$*"; }
err() { printf '%s\n' "$*" >&2; }

need_root() {
    [ "$(id -u)" -eq 0 ] || { err '请使用 root 运行 3m-ui 管理命令。 / Run as root.'; exit 1; }
}

confirm() {
    # usage: confirm "prompt" [default y|n]
    prompt="$1"
    def="${2:-n}"
    printf '%s [%s]: ' "$prompt" "$def"
    IFS= read -r ans || return 1
    [ -z "$ans" ] && ans="$def"
    case "$ans" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}

pause() {
    printf '\n%s' '按回车返回菜单 / Press Enter to return... '
    IFS= read -r _ || true
}

run_script() {
    script="$1"
    shift
    [ -x "$script" ] || {
        err "管理脚本不存在: $script"
        return 1
    }
    "$script" "$@"
}

run_app() {
    cmd="$1"
    shift
    if [ ! -x "$APP_BIN" ]; then
        err "未找到程序: $APP_BIN"
        return 1
    fi
    mkdir -p "$DATA_DIR"
    (cd "$DATA_DIR" && THREE_M_UI_CONFIG="$CONFIG_FILE" THREE_M_UI_DATA_DIR="$DATA_DIR" "$APP_BIN" "$cmd" "$@")
}

init_system() {
    if [ -d "$ROOT/run/systemd/system" ] && command -v systemctl >/dev/null 2>&1; then
        printf '%s' systemd
    elif command -v rc-service >/dev/null 2>&1; then
        printf '%s' openrc
    else
        printf '%s' unsupported
    fi
}

service_action() {
    action="$1"
    case "$(init_system)" in
        systemd) systemctl "$action" 3m-ui ;;
        openrc) rc-service 3m-ui "$action" ;;
        *) err '未检测到 systemd 或 OpenRC。'; return 1 ;;
    esac
}

service_is_active() {
    case "$(init_system)" in
        systemd) systemctl is-active --quiet 3m-ui ;;
        openrc) rc-service 3m-ui status >/dev/null 2>&1 ;;
        *) return 1 ;;
    esac
}

service_enable() {
    case "$(init_system)" in
        systemd)
            if systemctl enable 3m-ui; then
                say '已设置开机自启 / Enabled on boot.'
            else
                err 'enable 失败（服务单元是否已安装？）'
                return 1
            fi
            ;;
        openrc)
            rc-update add 3m-ui default && say '已设置开机自启 / Enabled on boot.'
            ;;
        *) err '不支持的 init'; return 1 ;;
    esac
}

service_disable() {
    case "$(init_system)" in
        systemd)
            if systemctl disable 3m-ui; then
                say '已取消开机自启 / Disabled on boot.'
            else
                err 'disable 失败'
                return 1
            fi
            ;;
        openrc)
            rc-update del 3m-ui default && say '已取消开机自启 / Disabled on boot.'
            ;;
        *) err '不支持的 init'; return 1 ;;
    esac
}

yaml_get() {
    # yaml_get KEY  e.g. "port:"  — pass the key WITHOUT leading indentation.
    #
    # Two properties are load-bearing:
    #   1. Scope — only the "server:" block is searched, so an identically
    #      named key elsewhere is never mistaken for the panel's.
    #   2. Indentation-agnostic — the key is matched on $1, not on a fixed
    #      prefix. The Go binary rewrites config.yaml through a YAML
    #      marshaller whose indent is not stable (2 spaces when init writes
    #      it, 4 after reset-config). Matching "  port:" literally made every
    #      menu action silently miss after an SSH rescue.
    key="$1"
    if [ ! -f "$CONFIG_FILE" ]; then
        return 1
    fi
    awk -v k="$key" '
        /^[^[:space:]#]/ { inblk = (/^server:/) }
        inblk && $1 == k { sub(/^[^:]+:[[:space:]]*/, ""); gsub(/["\047]/, ""); print; exit }
    ' "$CONFIG_FILE"
}

yaml_set() {
    # yaml_set KEY VALUE  — sets or inserts under the "server:" block.
    # Pass the key WITHOUT leading indentation (see yaml_get).
    #
    # Three properties are load-bearing here:
    #   1. Scope — only the first match inside "server:" is rewritten. The old
    #      `sed s|^  key:.*|` rewrote EVERY matching line, so a listener with
    #      the same indentation silently got the panel's port.
    #   2. Indentation — replacement preserves the indentation already used by
    #      the matched line, and insertion adopts the block's existing indent
    #      (defaulting to two spaces). A hardcoded indent produced a duplicate
    #      key — "    port:" from the Go marshaller plus "  port:" from us —
    #      while the menu still reported success.
    #   3. Permissions — we write back with `cat > file` instead of `mv`, so
    #      the original inode, owner and mode survive. `mv` replaced a 0600
    #      config.yaml (JWT + credential keys) with a umask-default 0644 file.
    key="$1"
    val="$2"
    if [ ! -f "$CONFIG_FILE" ]; then
        err "配置不存在: $CONFIG_FILE"
        return 1
    fi
    tmp=$(mktemp "${CONFIG_FILE}.tmp.XXXXXX") || return 1
    if awk -v k="$key" '
        /^[^[:space:]#]/ { inblk = (/^server:/) }
        inblk && $1 == k { found = 1 }
        END { exit found ? 0 : 1 }
    ' "$CONFIG_FILE"
    then
        # Key exists: rewrite it in place, keeping its own indentation.
        awk -v k="$key" -v v="$val" '
            /^[^[:space:]#]/ { inblk = (/^server:/) }
            inblk && $1 == k && !done {
                match($0, /^[[:space:]]*/)
                print substr($0, 1, RLENGTH) k " " v
                done = 1
                next
            }
            { print }
        ' "$CONFIG_FILE" > "$tmp"
    else
        # Key absent: append at the end of the "server:" block using that
        # block's indent. Fail loudly if there is no such block — silently
        # no-oping and then restarting would make the operator believe in a
        # change that never happened.
        if ! awk -v k="$key" -v v="$val" '
            /^[^[:space:]#]/ {
                if (inblk && !ins) { print (ind == "" ? "  " : ind) k " " v; ins = 1 }
                inblk = (/^server:/)
            }
            inblk && /^[[:space:]]+[^[:space:]]/ && ind == "" {
                match($0, /^[[:space:]]*/)
                ind = substr($0, 1, RLENGTH)
            }
            { print }
            END {
                if (inblk && !ins) { print (ind == "" ? "  " : ind) k " " v; ins = 1 }
                else if (!ins) exit 3
            }
        ' "$CONFIG_FILE" > "$tmp"
        then
            rm -f "$tmp"
            err "配置中未找到 server: 段，无法写入 ${key}（未做任何修改）"
            return 1
        fi
    fi
    cat "$tmp" > "$CONFIG_FILE" || { rm -f "$tmp"; return 1; }
    rm -f "$tmp"
}

# --- service ops -------------------------------------------------------------

status() {
    case "$(init_system)" in
        systemd) systemctl --no-pager --full status 3m-ui || true ;;
        openrc) rc-service 3m-ui status || true ;;
        *) say 'init: unsupported' ;;
    esac
    printf '\n'
    if service_is_active; then
        say '状态: 运行中 / running'
    else
        say '状态: 未运行 / stopped'
    fi
    case "$(init_system)" in
        systemd)
            if systemctl is-enabled --quiet 3m-ui 2>/dev/null; then
                say '开机自启: 是 / enabled'
            else
                say '开机自启: 否 / disabled'
            fi
            ;;
        openrc)
            if rc-update show default 2>/dev/null | grep -qw 3m-ui; then
                say '开机自启: 是 / enabled'
            else
                say '开机自启: 否 / disabled'
            fi
            ;;
    esac
    if [ -x "$APP_BIN" ]; then
        say "应用程序: $APP_BIN"
        "$APP_BIN" --version 2>/dev/null || true
    else
        say '应用程序二进制不存在。'
    fi
}

logs() {
    case "$(init_system)" in
        systemd)
            if [ "${1:-}" = "-f" ] || [ "${1:-}" = "follow" ]; then
                # Ctrl+C must not abort the interactive menu under set -e
                journalctl -u 3m-ui -f --no-pager || true
            else
                journalctl -u 3m-ui -n 100 --no-pager || true
            fi
            ;;
        openrc)
            if [ -f /var/log/3m-ui/3m-ui.log ]; then
                if [ "${1:-}" = "-f" ] || [ "${1:-}" = "follow" ]; then
                    tail -n 50 -f /var/log/3m-ui/3m-ui.log || true
                else
                    tail -n 100 /var/log/3m-ui/3m-ui.log || true
                fi
            else
                say '未找到日志文件 /var/log/3m-ui/3m-ui.log'
            fi
            ;;
        *) say '未找到可用的日志系统。' ;;
    esac
}

version() {
    printed=0
    if [ -x "$APP_BIN" ]; then
        out="$("$APP_BIN" --version 2>/dev/null || true)"
        [ -z "$out" ] && out="$("$APP_BIN" version 2>/dev/null || true)"
        if [ -n "$out" ]; then
            printf '%s\n' "$out"
            printed=1
        fi
    fi
    for f in VERSION CHANNEL MIHOMO_VERSION REPOSITORY; do
        if [ -s "$BASE/$f" ]; then
            case "$f" in
                VERSION) printf 'Installed release: %s\n' "$(cat "$BASE/$f")" ;;
                CHANNEL) printf 'Update channel: %s\n' "$(cat "$BASE/$f")" ;;
                MIHOMO_VERSION) printf 'Bundled Mihomo: %s\n' "$(cat "$BASE/$f")" ;;
                REPOSITORY) printf 'Release source: %s\n' "$(cat "$BASE/$f")" ;;
            esac
            printed=1
        fi
    done
    if [ -x "$APP_BIN" ]; then
        printf 'Binary: %s\n' "$APP_BIN"
        printed=1
    fi
    if [ "$printed" -eq 0 ]; then
        err '3m-ui 未安装或无法读取版本信息。'
        return 1
    fi
}

# --- config / access ---------------------------------------------------------

show_access() {
    port=$(yaml_get 'port:' 2>/dev/null || true)
    port="${port:-8080}"
    listen=$(yaml_get 'listen:' 2>/dev/null || true)
    pub=$(yaml_get 'public_url:' 2>/dev/null || true)
    web=$(yaml_get 'web_path:' 2>/dev/null || true)
    subp=$(yaml_get 'sub_path:' 2>/dev/null || true)
    subport=$(yaml_get 'sub_port:' 2>/dev/null || true)

    say "配置文件: $CONFIG_FILE"
    say "面板端口 / port: $port"
    [ -n "$listen" ] && say "监听 / listen: $listen"
    [ -n "$pub" ] && [ "$pub" != "null" ] && say "public_url: $pub"
    [ -n "$web" ] && say "web_path: $web"
    [ -n "$subp" ] && say "sub_path: $subp"
    [ -n "$subport" ] && [ "$subport" != "0" ] && say "sub_port: $subport"

    path_suffix=""
    case "$web" in
        /*) path_suffix="$web" ;;
        "") path_suffix="" ;;
        *) path_suffix="/$web" ;;
    esac

    if [ -n "$pub" ] && [ "$pub" != "null" ]; then
        # strip trailing slash for display
        base=$(printf '%s' "$pub" | sed 's|/*$||')
        say "面板访问 / Panel URI: ${base}${path_suffix}/"
    else
        say "面板访问示例 (请换成公网 IP/域名):"
        say "  http://<IP>:${port}${path_suffix}/"
        say "  https://<域名>/   (若已启用面板 SSL 且监听 443)"
    fi
    say '面板启用 HTTPS 后，请使用面板中配置的域名和 HTTPS 端口。'
    if run_app healthcheck >/dev/null 2>&1; then
        say '面板健康检查通过 / healthcheck OK'
    else
        say '面板健康检查未通过或未就绪 (可稍后重试)'
    fi
    say '管理命令: 3m-ui'
    say '初始密码在首次安装时随机生成；遗失可用菜单「重置管理员密码」。'
}

change_port() {
    if [ ! -f "$CONFIG_FILE" ]; then
        err "配置文件不存在: $CONFIG_FILE"
        return 1
    fi
    current=$(yaml_get 'port:' 2>/dev/null || echo "8080")
    printf '当前面板端口: %s\n' "$current"
    printf '输入新端口 (1-65535, 回车取消): '
    IFS= read -r new_port || return 0
    [ -z "$new_port" ] && return 0
    case "$new_port" in
        ''|*[!0-9]*) err '无效端口。'; return 1 ;;
    esac
    [ "$new_port" -ge 1 ] && [ "$new_port" -le 65535 ] || {
        err '端口必须在 1-65535 范围内。'
        return 1
    }
    [ "$new_port" = "$current" ] && { say '端口未变更。'; return 0; }
    yaml_set 'port:' "$new_port"
    say "端口已更改: $current → $new_port"
    say '正在重启 3m-ui...'
    service_action restart
    sleep 1
    if service_is_active; then
        say "✓ 已重启。配置端口: $new_port"
    else
        err '✗ 启动失败，请检查: 3m-ui logs'
    fi
}

set_public_url() {
    if [ ! -f "$CONFIG_FILE" ]; then
        err "配置不存在: $CONFIG_FILE"
        return 1
    fi
    cur=$(yaml_get 'public_url:' 2>/dev/null || true)
    printf '当前 public_url: %s\n' "${cur:-<empty>}"
    printf '输入新的 public_url (例如 https://example.com ，回车取消): '
    IFS= read -r url || return 0
    [ -z "$url" ] && return 0
    yaml_set 'public_url:' "\"$url\""
    say '已写入 public_url。'
    if confirm '是否立即重启面板?' y; then
        service_action restart
        sleep 1
        service_is_active && say '✓ 已重启' || err '✗ 重启后未运行'
    fi
}

set_panel_paths() {
    if [ ! -f "$CONFIG_FILE" ]; then
        err "配置不存在: $CONFIG_FILE"
        return 1
    fi
    say '留空则保持现有值。 / Leave blank to keep current.'
    cur_web=$(yaml_get 'web_path:' 2>/dev/null || true)
    cur_sub=$(yaml_get 'sub_path:' 2>/dev/null || true)
    cur_sp=$(yaml_get 'sub_port:' 2>/dev/null || true)
    cur_listen=$(yaml_get 'listen:' 2>/dev/null || true)
    printf 'web_path (面板路径前缀, 现: %s): ' "${cur_web:-<empty>}"
    IFS= read -r web || true
    printf 'sub_path (订阅路径前缀, 现: %s): ' "${cur_sub:-<empty>}"
    IFS= read -r sub || true
    printf 'sub_port (0=与面板同端口, 现: %s): ' "${cur_sp:-0}"
    IFS= read -r sp || true
    printf 'listen (空=全部接口, 现: %s): ' "${cur_listen:-<empty>}"
    IFS= read -r lis || true

    changed=0
    if [ -n "${web:-}" ]; then
        yaml_set 'web_path:' "\"$web\""
        changed=1
    fi
    if [ -n "${sub:-}" ]; then
        yaml_set 'sub_path:' "\"$sub\""
        changed=1
    fi
    if [ -n "${sp:-}" ]; then
        case "$sp" in
            ''|*[!0-9]*) err 'sub_port 无效' ;;
            *) yaml_set 'sub_port:' "$sp"; changed=1 ;;
        esac
    fi
    if [ -n "${lis:-}" ]; then
        yaml_set 'listen:' "\"$lis\""
        changed=1
    fi
    if [ "$changed" -eq 0 ]; then
        say '未修改。'
        return 0
    fi
    say '已写入配置。'
    if confirm '是否立即重启面板?' y; then
        service_action restart
        sleep 1
        service_is_active && say '✓ 已重启' || err '✗ 重启后未运行'
    fi
}

reset_admin() {
    if [ ! -x "$APP_BIN" ]; then
        err "未找到程序: $APP_BIN"
        return 1
    fi
    say '将随机生成新的管理员密码，并要求下次登录修改密码。'
    if ! confirm '确认继续?' n; then
        say '已取消。'
        return 0
    fi
    run_app reset-admin || return 1
}

reset_config() {
    if [ ! -x "$APP_BIN" ]; then
        err "未找到程序: $APP_BIN"
        return 1
    fi
    say '=== SSH 救援 / SSH Rescue ==='
    say '当面板配置错误（端口/SSL/监听地址）导致 Web 无法访问时，'
    say '可通过此菜单重置为安全默认值，重启后面板以 HTTP 模式启动。'
    say ''
    say '重置范围 / Scope:'
    say '  1) 仅面板 (listen + SSL)      — 最常用，仅 Web 打不开时'
    say '  2) 面板 + 订阅地址              — 同时清除 public_url'
    say '  3) 面板 + 订阅 + 节点公网地址   — 全量重置（含 access_profile）'
    say '  0) 返回 / Cancel'
    printf '选择 [0-3]: '
    IFS= read -r scope || return 0
    case "$scope" in
        1) flags='--panel --yes' ;;
        2) flags='--panel --public --yes' ;;
        3) flags='--all --yes' ;;
        *) say '已取消。'; return 0 ;;
    esac
    say ''
    say "将以 ${flags} 重置。该操作会停止面板、修改 config.yaml + DB，然后重启面板。"
    say '不会删除用户 / 节点 / 流量记录 / 凭证。'
    if ! confirm '确认继续?' n; then
        say '已取消。'
        return 0
    fi
    say ''
    say '停止 3m-ui 服务...'
    service_action stop >/dev/null 2>&1 || true
    if ! run_app reset-config $flags; then
        err '✗ reset-config 失败，请查看上方错误。面板将以旧配置重启。'
        service_action start || true
        return 1
    fi
    say ''
    say '重启 3m-ui 服务...'
    service_action start || true
    sleep 1
    if service_is_active; then
        say '✓ 重置完成。请用 HTTP（不是 HTTPS）访问面板:'
        port=$(yaml_get 'port:' 2>/dev/null || echo "8080")
        say "  http://<server-ip>:${port}/"
        say '在 Web UI 重新应用 SSL / 域名 / 订阅地址设置。'
    else
        err '✗ 面板启动失败，请检查: 3m-ui logs'
    fi
}

# --- BBR ---------------------------------------------------------------------

bbr_status() {
    if [ "$(uname -s)" != Linux ]; then
        err '仅支持 Linux。'
        return 1
    fi
    sysctl net.ipv4.tcp_congestion_control 2>/dev/null || true
    sysctl net.core.default_qdisc 2>/dev/null || true
}

enable_bbr() {
    if [ "$(uname -s)" != Linux ]; then
        err '仅支持 Linux。'
        return 1
    fi
    if sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr; then
        say 'BBR 已启用。'
        return 0
    fi
    modprobe tcp_bbr 2>/dev/null || true
    if [ ! -f /etc/sysctl.d/99-3m-ui-bbr.conf ]; then
        # /etc/sysctl.d is missing on some minimal/container images. Without
        # this mkdir the redirect fails and `set -e` aborts the whole menu.
        mkdir -p /etc/sysctl.d || { err '无法创建 /etc/sysctl.d'; return 1; }
        printf '%s\n' 'net.core.default_qdisc=fq' > /etc/sysctl.d/99-3m-ui-bbr.conf
        printf '%s\n' 'net.ipv4.tcp_congestion_control=bbr' >> /etc/sysctl.d/99-3m-ui-bbr.conf
    fi
    sysctl --system >/dev/null 2>&1 || sysctl -p /etc/sysctl.d/99-3m-ui-bbr.conf 2>/dev/null || true
    bbr_status
    say '已尝试启用 BBR。'
}

disable_bbr() {
    if [ "$(uname -s)" != Linux ]; then
        err '仅支持 Linux。'
        return 1
    fi
    rm -f /etc/sysctl.d/99-3m-ui-bbr.conf
    sysctl -w net.ipv4.tcp_congestion_control=cubic >/dev/null 2>&1 || true
    sysctl -w net.core.default_qdisc=fq_codel >/dev/null 2>&1 || true
    bbr_status
    say '已尝试恢复默认拥塞控制 (cubic)。重启后仍可能被其它 sysctl 覆盖。'
}

bbr_menu() {
    say ''
    say '  1) 状态 / Status'
    say '  2) 启用 BBR / Enable'
    say '  3) 关闭 BBR / Disable'
    say '  0) 返回'
    printf '选择: '
    IFS= read -r c || return 0
    case "$c" in
        1) bbr_status ;;
        2) enable_bbr ;;
        3) disable_bbr ;;
        0|'') ;;
        *) say '无效选择，请重新输入。' ;;
    esac
}

# --- Geo / firewall / SSL helpers --------------------------------------------

update_geo() {
    # Geo databases influence routing. Do not download a moving "latest" asset
    # from a root-owned management script without a release checksum.
    say 'Geo 数据库请在面板「系统设置 → 网络与 Geo」中更新。'
    say 'Use Settings → Network & Geo in the authenticated panel to update Geo files.'
    say '该入口会显示每个文件的更新结果；管理脚本不会下载未校验的 latest 资产。'
}

open_firewall() {
    say '将放行 22 / 80 / 443 与面板端口，这会扩大公网暴露面。'
    if ! confirm '确认继续?' n; then
        say '已取消。'
        return 0
    fi
    say '尝试放行常用端口 (22, 80, 443, 面板端口)...'
    port=$(yaml_get 'port:' 2>/dev/null || echo "8080")
    if command -v ufw >/dev/null 2>&1; then
        ufw allow 22/tcp || true
        ufw allow 80/tcp || true
        ufw allow 443/tcp || true
        ufw allow "${port}/tcp" || true
        say '已向 ufw 提交规则 (请自行 ufw enable / status)。'
    elif command -v firewall-cmd >/dev/null 2>&1; then
        firewall-cmd --permanent --add-port=80/tcp || true
        firewall-cmd --permanent --add-port=443/tcp || true
        firewall-cmd --permanent --add-port="${port}/tcp" || true
        firewall-cmd --reload || true
        say '已尝试 firewalld 放行。'
    else
        say '未检测到 ufw/firewalld。请在云安全组放行 80/443 与面板端口。'
    fi
}

ssl_cert_menu() {
    say ''
    say '面板 HTTPS 推荐在「系统设置 → SSL」使用内置 ACME。'
    say '以下为可选：用外部工具申请 PEM，供节点或反代使用。'
    say ''
    say '  1) 打印 certbot 命令 (standalone)'
    say '  2) 打印 acme.sh 命令 (standalone)'
    say '  3) 打印 Cloudflare DNS (acme.sh) 示例'
    say '  0) 返回'
    printf '选择: '
    IFS= read -r c || return 0
    # 0 / 空输入先返回主菜单，不要继续索要域名。
    case "$c" in
        1|2|3) ;;
        0|'') return 0 ;;
        *) say '无效选择，请重新输入。'; return 0 ;;
    esac
    printf '域名 (例 dzx.qzz.io): '
    IFS= read -r dom || return 0
    if [ -z "$dom" ]; then return 0; fi
    printf '邮箱 (可空): '
    IFS= read -r em || true
    case "$c" in
        1)
            say ''
            say '# 申请前请确保 80 未被占用（可临时: systemctl stop 3m-ui）'
            if [ -n "$em" ]; then
                say "certbot certonly --standalone -d $dom --email $em --agree-tos --non-interactive"
            else
                say "certbot certonly --standalone -d $dom --register-unsafely-without-email --agree-tos --non-interactive"
            fi
            say "# 证书: /etc/letsencrypt/live/$dom/fullchain.pem"
            say "# 私钥: /etc/letsencrypt/live/$dom/privkey.pem"
            ;;
        2)
            say ''
            say '# 以下仅为示例命令，请自行确认来源后再执行'
            say 'curl https://get.acme.sh | sh'
            say "~/.acme.sh/acme.sh --set-default-ca --server letsencrypt"
            say "~/.acme.sh/acme.sh --issue -d $dom --standalone"
            say "~/.acme.sh/acme.sh --install-cert -d $dom \\"
            say "  --key-file /etc/3m-ui/certs/${dom}.key \\"
            say "  --fullchain-file /etc/3m-ui/certs/${dom}.crt \\"
            say "  --reloadcmd \"systemctl restart 3m-ui\""
            ;;
        3)
            say ''
            say 'export CF_Token="your_api_token"'
            say "~/.acme.sh/acme.sh --issue -d $dom --dns dns_cf"
            say "# 不占用 80，适合面板已占用 ACME 端口时给节点另签证书"
            ;;
        *) ;;
    esac
}

# --- update management script itself -----------------------------------------

update_self() {
    # Lifecycle scripts and the panel binary are a single release unit. Routing
    # this action through the installer keeps the checksum, provenance option,
    # rollback snapshot, and health check that protect normal updates.
    say '管理脚本会随已验证的面板版本一起更新。'
    if ! confirm '通过已验证的发布更新 3m-ui（包括管理脚本）？' n; then
        say '已取消。'
        return 0
    fi
    run_script "$UPDATER" || return 1
}

update_to_version() {
    printf '输入版本号 (如 v1.1.1，回车取消): '
    IFS= read -r ver || return 0
    [ -z "$ver" ] && return 0
    case "$ver" in
        v*) ;;
        *) ver="v$ver" ;;
    esac
    say "将更新到 $ver ..."
    run_script "$UPDATER" "$ver" || return 1
}

# --- menu --------------------------------------------------------------------

menu() {
    while :; do
        printf '\n'
        printf '%s\n' '╭──────────────────────────────────────────────╮'
        printf '%s\n' '│     3m-ui 管理控制台 / Management Console     │'
        printf '%s\n' '│        Mihomo Core Management UI             │'
        printf '%s\n' '╰──────────────────────────────────────────────╯'
        printf '\n'
        printf '%s\n' '  1. 安装/修复 | Install / Repair'
        printf '%s\n' '  2. 更新到最新 | Update latest'
        printf '%s\n' '  3. 更新到指定版本 | Update to version'
        printf '%s\n' '  4. 运行状态 | Status'
        printf '%s\n' '  5. 启动 | Start'
        printf '%s\n' '  6. 重启 | Restart'
        printf '%s\n' '  7. 停止 | Stop'
        printf '%s\n' '  8. 开机自启 开/关 | Enable/Disable'
        printf '%s\n' '  9. 日志 | Logs'
        printf '%s\n' ' 10. 版本 | Version'
        printf '%s\n' ' 11. 访问信息 / URI | Access info'
        printf '%s\n' ' 12. 修改面板端口 | Change port'
        printf '%s\n' ' 13. 设置 public_url | Public URL'
        printf '%s\n' ' 14. 路径/订阅端口 | web_path sub_path sub_port'
        printf '%s\n' ' 15. 重置管理员密码 | Reset admin'
        printf '%s\n' ' 16. SSH 救援重置面板配置 | Reset-config (SSH rescue)'
        printf '%s\n' ' 17. 健康检查 | Healthcheck'
        printf '%s\n' ' 18. BBR | BBR menu'
        printf '%s\n' ' 19. 更新 Geo | Update Geo'
        printf '%s\n' ' 20. 防火墙放行提示 | Firewall'
        printf '%s\n' ' 21. SSL 证书助手 | SSL cert helper'
        printf '%s\n' ' 22. 备份 | Backup'
        printf '%s\n' ' 23. 更新 3m-ui（含管理脚本） | Update 3m-ui (incl. scripts)'
        printf '%s\n' '  a. 卸载 | Uninstall'
        printf '%s\n' '  0. 退出 | Exit'
        printf '\n请选择 / Select: '
        IFS= read -r choice || exit 0
        case "$choice" in
            1) run_script "$INSTALLER" || true; pause ;;
            2) run_script "$UPDATER" || true; pause ;;
            3) update_to_version || true; pause ;;
            4) status || true; pause ;;
            5) service_action start || true; pause ;;
            6) service_action restart || true; pause ;;
            7) service_action stop || true; pause ;;
            8)
                printf 'e=启用 enable / d=禁用 disable: '
                ed=''
                IFS= read -r ed || true
                case "$ed" in
                    e|E) service_enable || true ;;
                    d|D) service_disable || true ;;
                    '') say '未变更。' ;;
                    *) say "无效输入: $ed（请输入 e 或 d）" ;;
                esac
                pause
                ;;
            9)
                printf '跟随输出? [y/N]: '
                IFS= read -r fl || true
                case "$fl" in y|Y) logs -f || true ;; *) logs || true; pause ;; esac
                ;;
            10) version || true; pause ;;
            11) show_access || true; pause ;;
            12) change_port || true; pause ;;
            13) set_public_url || true; pause ;;
            14) set_panel_paths || true; pause ;;
            15) reset_admin || true; pause ;;
            16) reset_config || true; pause ;;
            17)
                # The binary's healthcheck exits 0 silently on success, so a
                # bare `run_app healthcheck` made this item look inert.
                if run_app healthcheck; then
                    say '✓ 健康检查通过 / healthcheck OK'
                else
                    say '✗ 健康检查未通过 / healthcheck failed'
                    say '  详情: 3m-ui logs'
                fi
                pause ;;
            18) bbr_menu || true; pause ;;
            19) update_geo || true; pause ;;
            20) open_firewall || true; pause ;;
            21) ssl_cert_menu || true; pause ;;
            22) run_script "$INSTALLER" --backup || true; pause ;;
            23) update_self || true; pause ;;
            a|A) run_script "$UNINSTALLER" || true; pause ;;
            0) exit 0 ;;
            *) say '无效选择，请重新输入。' ;;
        esac
    done
}

# --- main --------------------------------------------------------------------


channel_cmd() {
    ch_file="$BASE/CHANNEL"
    arg="${1:-}"
    do_update=0
    if [ "$#" -gt 0 ]; then
        shift
    fi
    for a in "$@"; do
        case "$a" in --update|update|-u) do_update=1 ;; esac
    done
    if [ -z "$arg" ]; then
        cur=stable
        if [ -s "$ch_file" ]; then
            cur=$(tr -d '\n\r' < "$ch_file")
        fi
        case "$cur" in pre|prerelease) cur=pre ;; *) cur=stable ;; esac
        say "Current update channel: $cur"
        say "Switch: 3m-ui channel stable | 3m-ui channel pre"
        say "Switch and upgrade now: 3m-ui channel pre --update"
        return 0
    fi
    case "$arg" in
        stable|pre) ;;
        prerelease) arg=pre ;;
        *)
            err "Unknown channel: $arg (use stable or pre)"
            return 1
            ;;
    esac
    mkdir -p "$BASE"
    printf '%s\n' "$arg" > "$ch_file"
    say "Update channel set to: $arg (saved in $ch_file)"
    if [ "$do_update" -eq 1 ]; then
        say "Running update on channel $arg ..."
        THREE_M_UI_CHANNEL="$arg" run_script "$UPDATER" || return 1
    else
        say "Next: 3m-ui update   # install latest $arg release"
        say "  or: 3m-ui channel $arg --update"
    fi
}

main() {
    cmd=${1:-}
    case "$cmd" in
        help|-h|--help) ;;
        *) need_root ;;
    esac
    case "$cmd" in
        status) status ;;
        start) service_action start ;;
        restart) service_action restart ;;
        stop) service_action stop ;;
        enable) service_enable ;;
        disable) service_disable ;;
        logs) shift; logs "$@" ;;
        version) version ;;
        channel) shift; channel_cmd "$@" ;;
        config) shift; run_script "$MANAGER" config "$@" ;;
        backup) shift; run_script "$INSTALLER" --backup "$@" ;;
        restore) shift; run_script "$INSTALLER" --restore "$@" ;;
        init|reset-admin|healthcheck) run_app "$cmd" ;;
        reset-config)
            # reset-config needs the service stopped before the Go binary
            # runs (it writes config.yaml + DB rows the running panel caches).
            # --help / -h short-circuits — no stop/restart needed.
            shift # drop 'reset-config' so $@ holds the flags
            help_only=0
            for a in "$@"; do
                case "$a" in --help|-h) help_only=1 ;; esac
            done
            if [ "$help_only" = 1 ]; then
                run_app reset-config "$@"
            else
                say '停止 3m-ui 服务...'
                service_action stop >/dev/null 2>&1 || true
                if ! run_app reset-config "$@"; then
                    err '✗ reset-config 失败，请查看上方错误。'
                    service_action start || true
                    exit 1
                fi
                say '重启 3m-ui 服务...'
                service_action start || true
            fi
            ;;
        update|upgrade)
            shift
            run_script "$UPDATER" "$@"
            ;;
        install|fix)
            shift
            run_script "$INSTALLER" "$@"
            ;;
        uninstall|remove)
            shift
            run_script "$UNINSTALLER" "$@"
            ;;
        menu) menu ;;
        help|-h|--help)
            printf '%s\n' '用法: 3m-ui <命令> [参数]'
            printf '%s\n' ''
            printf '%s\n' '服务: status | start | restart | stop | enable | disable | logs | version | channel'
            printf '%s\n' '配置: config show | config port <端口>'
            printf '%s\n' '安装: update [vX.Y.Z|pre] | channel stable|pre [--update] | install | uninstall [--purge]'
            printf '%s\n' '数据: backup | restore <snapshot.tar.gz> | reset-admin | reset-config | healthcheck'
            printf '%s\n' '其他: menu | help'
            printf '%s\n' ''
            printf '%s\n' 'SSH 救援 / SSH rescue:'
            printf '%s\n' '  3m-ui reset-config --panel --yes   # 重置 listen/SSL，重启后 HTTP 访问'
            printf '%s\n' '  3m-ui reset-config --all --yes      # 全量重置（含 public_url + access_profile）'
            printf '%s\n' ''
            printf '%s\n' '不带参数时进入交互式菜单。'
            ;;
        '') menu ;;
        *)
            err "未知命令: $cmd"
            err '运行: 3m-ui help'
            exit 2
            ;;
    esac
}

main "$@"
