#!/bin/bash
# gxde-k9-chocker —— GXDE K9 的"项圈"管理工具
# 提供：查看任务、管理（新增/删除）配置、交互式新增、查看与导出当次运行日志
# 同时支持人类友好的交互式 TUI 菜单（基于 whiptail/dialog）与脚本友好的子命令调用。

set -u

# ===================== 基础变量 =====================
PROG_NAME="gxde-k9-chocker"
VERSION="1.9.3"

BASE_DIR_SYSTEM="${GXDE_K9_BASE_DIR_SYSTEM:-/usr/share/gxde-k9}"
BASE_DIR_USER="${GXDE_K9_BASE_DIR_USER:-${HOME}/.local/share/GXDE/gxde-k9}"
LOG_DIR_USER="${GXDE_K9_LOG_DIR_USER:-${HOME}/.local/share/GXDE/gxde-k9/logs/}"

# 支持的任务分类
CATEGORIES=(slimy timer shot edging)

# ===================== 样式定义 =====================
# 颜色（用于命令行输出；TUI 内部由 whiptail/dialog 自行配色）
C_RED=$'\e[31m'
C_GREEN=$'\e[32m'
C_YELLOW=$'\e[33m'
C_BLUE=$'\e[34m'
C_MAGENTA=$'\e[35m'
C_CYAN=$'\e[36m'
C_GRAY=$'\e[90m'
C_DIM=$'\e[2m'
C_BOLD=$'\e[1m'
C_RESET=$'\e[0m'

# 制表符
T_TOP="╭" T_TB="─" T_TJ="┬" T_TE="╮"
T_BOT="╰" T_BB="─" T_BJ="┴" T_BE="╯"
T_L="├" T_R="┤" T_MID="│" T_CROSS="┼"
T_H="─"
T_TL="╭" T_TR="╮"
T_BL="╰" T_BR="╯"

# ===================== 日志/输出辅助 =====================
log.warn()  { echo -e "${C_YELLOW}${C_BOLD}◆ WARN${C_RESET}  $*" >&2; }
log.error() { echo -e "${C_RED}${C_BOLD}✘ ERROR${C_RESET} $*" >&2; }
log.info()  { echo -e "${C_CYAN}${C_BOLD}● INFO${C_RESET}  $*" >&2; }
log.ok()    { echo -e "${C_GREEN}${C_BOLD}✔ OK${C_RESET}    $*" >&2; }

# 画一根横线
hline() {
    local w=${1:-60}
    printf "${C_GRAY}${C_DIM}"
    printf '%*s' "$w" '' | sed "s/ /${T_H}/g"
    printf "${C_RESET}\n"
}

# 画上边框
box_top() {
    local w=${1:-60}
    echo -ne "${C_GRAY}${C_DIM}${T_TOP}"
    printf '%*s' "$((w-2))" '' | sed "s/ /${T_TB}/g"
    echo -e "${T_TE}${C_RESET}"
}

# 画下边框
box_bottom() {
    local w=${1:-60}
    echo -ne "${C_GRAY}${C_DIM}${T_BL}"
    printf '%*s' "$((w-2))" '' | sed "s/ /${T_BB}/g"
    echo -e "${T_BR}${C_RESET}"
}

# 带框的标题
box_title() {
    local title="$1" w=${2:-60}
    local pad=$(( (w - ${#title} - 4) / 2 ))
    echo -ne "${C_GRAY}${C_DIM}${T_TOP}${T_TB}"
    printf '%*s' "$pad" '' | sed "s/ /${T_TB}/g"
    echo -ne "${C_BOLD}${C_CYAN} ${title} ${C_RESET}${C_GRAY}${C_DIM}"
    printf '%*s' "$(( w - pad - ${#title} - 4 ))" '' | sed "s/ /${T_TB}/g"
    echo -e "${T_TE}${C_RESET}"
}

# 彩色标签
tag_scope() {
    local scope="$1"
    if [[ "$scope" == "system" ]]; then
        echo -ne "${C_YELLOW}sys ${C_RESET}"
    else
        echo -ne "${C_GREEN}user${C_RESET}"
    fi
}

tag_category() {
    local cat="$1"
    case "$cat" in
        slimy)  echo -ne "${C_MAGENTA}slimy ${C_RESET}";;
        timer)  echo -ne "${C_BLUE}timer ${C_RESET}";;
        shot)   echo -ne "${C_CYAN}shot  ${C_RESET}";;
        edging) echo -ne "${C_YELLOW}edging${C_RESET}";;
        *)      printf '%-6s' "$cat";;
    esac
}

# ===================== 终端尺寸检测 =====================
detect_term_width() {
    local w
    w=$(tput cols 2>/dev/null || echo 80)
    # cap at 88, floor at 50
    [[ "$w" -gt 88 ]] && w=88
    [[ "$w" -lt 50 ]] && w=50
    echo "$w"
}

# 获取终端宽度，供后续 TUI 函数使用
_TERM_W=$(detect_term_width)
_tw() { detect_term_width; }

# ===================== TUI 后端检测 =====================
# 优先使用 whiptail（系统通常预装），其次 dialog；为空表示均不可用
TUI_BACKEND=""
detect_tui_backend() {
    [[ -n "${TUI_BACKEND:-}" ]] && return
    if command -v whiptail >/dev/null 2>&1; then
        TUI_BACKEND="whiptail"
    elif command -v dialog >/dev/null 2>&1; then
        TUI_BACKEND="dialog"
    fi
}

# 交互模式必须的 TUI 检查；缺失则给出安装提示并退出
require_tui() {
    detect_tui_backend
    if [[ -z "$TUI_BACKEND" ]]; then
        cat >&2 <<EOF

${C_RED}${C_BOLD}✘ 缺少 TUI 依赖${C_RESET}

${C_YELLOW}交互式菜单需要 ${C_BOLD}whiptail${C_RESET}${C_YELLOW} 或 ${C_BOLD}dialog${C_RESET}${C_YELLOW}。${C_RESET}

${C_CYAN}安装方式:${C_RESET}
  Debian/Ubuntu/GXDE:  ${C_BOLD}sudo apt install libnewt${C_RESET}      (提供 whiptail)
                       ${C_BOLD}sudo apt install dialog${C_RESET}
  Arch Linux:          ${C_BOLD}sudo pacman -S libnewt${C_RESET}        (whiptail)
                       ${C_BOLD}sudo pacman -S dialog${C_RESET}
  Termux:              ${C_BOLD}pkg install libnewt${C_RESET}           (whiptail)
                       ${C_BOLD}pkg install dialog${C_RESET}

${C_DIM}也可改用非交互子命令:${C_RESET}
  ${PROG_NAME} list
  ${PROG_NAME} add timer --name hi --schedule '* * * * *' --command 'echo hi'
  ${PROG_NAME} --help
EOF
        exit 1
    fi
}

# ===================== TUI 包装器 =====================
# 公共 backtitle（每次进入主菜单时刷新）
_BACKTITLE="GXDE K9 Chocker v${VERSION}"

_refresh_backtitle() {
    local user_count=0 sys_count=0 log_count=0
    local cat f d
    for cat in "${CATEGORIES[@]}"; do
        d=$(category_dir "$cat" user)
        [[ -d "$d" ]] && while IFS= read -r -d '' f; do user_count=$((user_count+1)); done < <(find "$d" -maxdepth 1 \( -name "*.$cat" -o -name "*.$cat.disabled" \) -type f -print0 2>/dev/null)
        d=$(category_dir "$cat" system)
        [[ -d "$d" ]] && while IFS= read -r -d '' f; do sys_count=$((sys_count+1)); done < <(find "$d" -maxdepth 1 \( -name "*.$cat" -o -name "*.$cat.disabled" \) -type f -print0 2>/dev/null)
    done
    if [[ -d "$LOG_DIR_USER" ]]; then
        while IFS= read -r -d '' _; do log_count=$((log_count+1)); done < <(find "$LOG_DIR_USER" -name '*.log' -type f -print0 2>/dev/null)
    fi
    _BACKTITLE="GXDE K9 Chocker v${VERSION}   |   配置: ${user_count} user / ${sys_count} system   |   日志: ${log_count} 个"
}

# 计算菜单/对话框尺寸：返回 "h w"
# items = 菜单项数；宽度上限放宽到 140，尽量占满终端
_tui_size() {
    local items=${1:-0}
    local h w
    h=$(tput lines 2>/dev/null || echo 24)
    w=$(tput cols 2>/dev/null || echo 80)
    (( w > 140 )) && w=140
    (( w < 60 )) && w=60
    # 高度：items + 7，但不超过终端高度 - 2，至少 8
    local need=$(( items + 7 ))
    (( need > h - 2 )) && need=$(( h - 2 ))
    (( need < 8 )) && need=8
    echo "$need $w"
}

# 计算详情/大文本展示尺寸：尽量占满终端（留 2 行/2 列边距）
_tui_size_full() {
    local h w
    h=$(tput lines 2>/dev/null || echo 24)
    w=$(tput cols 2>/dev/null || echo 80)
    (( h > 4 )) && h=$(( h - 2 ))
    (( w > 4 )) && w=$(( w - 2 ))
    echo "$h $w"
}

# 统一调用底层工具；用 3>&1 1>&2 2>&3 把 stdout 让给选择结果
_tui_run() {
    if [[ "$TUI_BACKEND" == "dialog" ]]; then
        dialog "$@"
    else
        whiptail "$@"
    fi
}

# tui_menu <title> <text> <tag1> <desc1> [<tag2> <desc2> ...]
# 返回（stdout）: 选中的 tag；取消时返回码 1
tui_menu() {
    local title="$1"; shift
    local text="$1"; shift
    local count=$(( $# / 2 ))
    local h w
    read -r h w < <(_tui_size "$count")
    _tui_run --backtitle "${_BACKTITLE}" --title "$title" --menu "$text" "$h" "$w" "$count" "$@" 3>&1 1>&2 2>&3
}

# tui_checklist <title> <text> <tag1> <desc1> <state1> [...]
# state: on / off
# 返回（stdout）: 选中项以换行分隔；取消时返回码 1
tui_checklist() {
    local title="$1"; shift
    local text="$1"; shift
    local count=$(( $# / 3 ))
    local h w
    read -r h w < <(_tui_size "$count")
    _tui_run --separate-output --backtitle "${_BACKTITLE}" --title "$title" --checklist "$text" "$h" "$w" "$count" "$@" 3>&1 1>&2 2>&3
}

# tui_inputbox <title> <text> [default]
tui_inputbox() {
    local title="$1" text="$2" default="${3:-}"
    local h w
    read -r h w < <(_tui_size 0)
    if [[ -n "$default" ]]; then
        _tui_run --backtitle "${_BACKTITLE}" --title "$title" --inputbox "$text" "$h" "$w" "$default" 3>&1 1>&2 2>&3
    else
        _tui_run --backtitle "${_BACKTITLE}" --title "$title" --inputbox "$text" "$h" "$w" 3>&1 1>&2 2>&3
    fi
}

# tui_yesno <title> <text>  -> 0=yes 1=no
tui_yesno() {
    local title="$1" text="$2"
    local h w
    read -r h w < <(_tui_size 0)
    _tui_run --backtitle "${_BACKTITLE}" --title "$title" --yesno "$text" "$h" "$w"
}

# tui_msgbox <title> <text>
tui_msgbox() {
    local title="$1" text="$2"
    local h w
    read -r h w < <(_tui_size 0)
    _tui_run --backtitle "${_BACKTITLE}" --title "$title" --msgbox "$text" "$h" "$w"
}

# tui_textbox <title> <file>  滚动查看文本文件（尽量占满终端）
tui_textbox() {
    local title="$1" file="$2"
    local h w
    read -r h w < <(_tui_size_full)
    if [[ "$TUI_BACKEND" == "dialog" ]]; then
        dialog --backtitle "${_BACKTITLE}" --title "$title" --textbox "$file" "$h" "$w"
    else
        whiptail --backtitle "${_BACKTITLE}" --title "$title" --scrolltext --textbox "$file" "$h" "$w"
    fi
}

# 从 stdin 读取内容，剥离 ANSI 转义后用 textbox 展示
tui_show_stdin() {
    local title="$1"
    local tmp
    tmp=$(mktemp /tmp/k9-view-XXXXXX.txt)
    sed -r 's/\x1B\[[0-9;]*[mK]//g' > "$tmp"
    if [[ -s "$tmp" ]]; then
        tui_textbox "$title" "$tmp"
    else
        tui_msgbox "$title" "（无内容）"
    fi
    rm -f "$tmp"
}

# 简单确认对话框（兼容旧调用名）
confirm_action() {
    local msg="${1:-确认执行？}"
    tui_yesno "请确认" "$msg"
}

# ===================== 风险提示/提权 =====================
confirm_system_change() {
    # 若 TUI 可用且 stdin 是终端，使用 TUI 风格的确认
    detect_tui_backend
    if [[ -n "$TUI_BACKEND" ]] && [[ -t 0 ]]; then
        tui_yesno "⚠ 风险操作" \
            "您即将修改 /usr/share 下的系统级配置。\n系统配置误修改可能导致服务无法启动或 K9 自身异常。\n建议在操作前备份相关目录。\n\n是否继续？"
        return $?
    fi
    # 命令行回退（保持原行为）
    echo
    echo -e "  ${C_RED}${C_BOLD}⚠ 风险操作${C_RESET}"
    echo
    echo -e "  ${C_YELLOW}您即将修改 /usr/share 下的系统级配置。${C_RESET}"
    echo -e "  ${C_YELLOW}系统配置误修改可能导致服务无法启动或 K9 自身异常。${C_RESET}"
    echo -e "  ${C_YELLOW}建议在操作前备份相关目录。${C_RESET}"
    echo
    read -r -p "  输入 ${C_BOLD}yes${C_RESET} 确认： " ans 2>&1
    [[ "$ans" == "yes" || "$ans" == "y" || "$ans" == "YES" ]]
}

elevate_cmd() {
    if command -v pkexec >/dev/null 2>&1; then
        echo "pkexec"
    elif command -v sudo >/dev/null 2>&1; then
        echo "sudo"
    else
        echo ""
    fi
}

# ===================== 路径解析 =====================
category_dir() {
    local cat="$1" scope="${2:-user}"
    if [[ "$scope" == "system" ]]; then
        echo "${BASE_DIR_SYSTEM}/${cat}/"
    else
        echo "${BASE_DIR_USER}/${cat}/"
    fi
}

ensure_dir() {
    local d="$1"
    [[ -d "$d" ]] || mkdir -p "$d"
}

numfmt_size() {
    local b=$1
    if (( b >= 1048576 )); then
        printf "%.1fMB" "$(echo "scale=1; $b/1048576" | bc -l 2>/dev/null || echo "$((b/1048576)).0")"
    elif (( b >= 1024 )); then
        printf "%.1fKB" "$(echo "scale=1; $b/1024" | bc -l 2>/dev/null || echo "$((b/1024)).0")"
    else
        printf "%dB" "$b"
    fi
}

# ===================== 帮助 =====================
print_help() {
    cat << EOF
${C_BOLD}${PROG_NAME} v${VERSION}${C_RESET} — ${C_CYAN}GXDE K9 任务与配置管理工具${C_RESET}

${C_DIM}用法:${C_RESET}
  ${PROG_NAME}                       进入交互式 TUI 菜单（需 whiptail 或 dialog）
  ${PROG_NAME} <子命令> [选项...]

${C_DIM}子命令:${C_RESET}
  list       [--category <cat>] [--json]      ${C_GRAY}列出任务及启用状态${C_RESET}
  add        <category> [选项]                 ${C_GRAY}新增已启用任务${C_RESET}
  start      <category> <name> [--system|--user]       ${C_GRAY}仅启动已启用任务一次（无需提权）${C_RESET}
  restart    <category> <name> [--system|--user]       ${C_GRAY}停止后立即启动已启用任务（无需提权）${C_RESET}
  stop       <category> <name> [--system|--user]       ${C_GRAY}仅停止关联进程，保留任务状态（无需提权）${C_RESET}
  enable     <category> <name> [--system|--user] [--yes] ${C_GRAY}仅启用任务（改后缀，系统级需提权）${C_RESET}
  disable    <category> <name> [--system|--user] [--yes] ${C_GRAY}停止关联进程并禁用任务（系统级需提权）${C_RESET}
  remove     <category> <name> [--system|--user] [--yes] ${C_GRAY}停止并删除配置（系统级需提权）${C_RESET}
  import     <category> <file> [--name N]     ${C_GRAY}从外部导入脚本${C_RESET}
  logs       <子命令> [...]                    ${C_GRAY}日志查看与导出${C_RESET}
  status     [<category> <name>] [--system|--user] ${C_GRAY}查看总览或任务状态${C_RESET}
  help                                       ${C_GRAY}显示此帮助${C_RESET}
  version                                    ${C_GRAY}显示版本号${C_RESET}

${C_DIM}分类:${C_RESET}
  $(for c in "${CATEGORIES[@]}"; do echo -n "$(tag_category "$c") "; done)

${C_DIM}通用选项:${C_RESET}
  --system          操作系统级配置（/usr/share）
  --user            操作用户级配置（默认）
  --yes             跳过交互确认（脚本调用）
  --json            JSON 输出（list 子命令）

${C_DIM}提权说明:${C_RESET}
  ${C_GRAY}system 指系统安装的用户态服务（配置位于 /usr/share/gxde-k9）。其中仅 enable/disable/remove${C_RESET}
  ${C_GRAY}会修改系统配置文件，需提权；add/import 创建系统配置同样需提权。${C_RESET}
  ${C_GRAY}start/stop/restart 为运行时操作，仅读取配置并管理进程，无需提权。${C_RESET}

${C_DIM}任务状态与停止范围:${C_RESET}
  启用任务使用 .<category>，禁用任务使用 .<category>.disabled；守护程序仅扫描启用文件。
  enable 仅改文件名后缀，不启动任务。disable 会先停止关联进程再改文件名后缀。start/restart 仅适用于已启用的 edging、slimy、shot；
  timer 仅支持 enable/disable，按计划由守护程序扫描。stop 只结束安全记录或明确引用任务文件的进程，
  不改后缀；edging/slimy 停止后仍会被守护程序重新启动，需 disable 才能永久阻止。

${C_DIM}add 详细:${C_RESET}
  add edging  --name N --target "<cmd>" [--no-systemd]
  add timer   --name N --schedule "<cron>" --command "<cmd>"
  add slimy   --name N --command "<cmd>"
  add shot    --name N --command "<cmd>"

${C_DIM}logs 子命令:${C_RESET}
  logs list                                  列出所有任务日志
  logs show   <category> <name>              查看某任务日志
  logs export <category> <name> <dest>       导出日志
  logs clear                                 清空日志

${C_DIM}示例:${C_RESET}
  ${PROG_NAME} list
  ${PROG_NAME} list --json
  ${PROG_NAME} list --category timer --json
  ${PROG_NAME} add timer --name hello --schedule '* * * * *' --command 'echo hi'
  ${PROG_NAME} remove timer hello

${C_DIM}交互式 TUI 依赖:${C_RESET}
  ${C_GRAY}whiptail (libnewt) 或 dialog${C_RESET}
  Debian/GXDE: ${C_GRAY}sudo apt install libnewt${C_RESET}    Termux: ${C_GRAY}pkg install libnewt${C_RESET}

${C_DIM}日志目录:${C_RESET}
  ${C_GRAY}${LOG_DIR_USER}${C_RESET}
EOF
}

# ===================== 任务状态与控制 =====================
valid_category() {
    local c
    for c in "${CATEGORIES[@]}"; do [[ "$c" == "$1" ]] && return 0; done
    return 1
}

task_paths() {
    local cat="$1" name="$2" scope="$3" d
    d=$(category_dir "$cat" "$scope")
    TASK_ENABLED="${d}${name}.${cat}"
    TASK_DISABLED="${TASK_ENABLED}.disabled"
}

pid_file_for() {
    local cat="$1" name="$2"
    local d="/tmp/GXDE/gxde-k9/$UID"
    printf '%s/%s.pid' "$d" "$name"
}

parse_task_action() {
    ACTION_CAT="" ACTION_NAME="" ACTION_SCOPE="user" ACTION_YES=0
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --system) ACTION_SCOPE="system";;
            --user) ACTION_SCOPE="user";;
            --yes) ACTION_YES=1;;
            *)
                if [[ -z "$ACTION_CAT" ]]; then ACTION_CAT="$1"
                elif [[ -z "$ACTION_NAME" ]]; then ACTION_NAME="$1"
                else log.error "多余参数: $1"; return 2; fi;;
        esac
        shift
    done
    [[ -n "$ACTION_CAT" && -n "$ACTION_NAME" ]] || { log.error "需要参数 <category> <name>"; return 2; }
    valid_category "$ACTION_CAT" || { log.error "未知分类: $ACTION_CAT"; return 2; }
}

elevate_task_action() {
    local action="$1"; shift
    [[ "$ACTION_SCOPE" != "system" || "$EUID" -eq 0 ]] && return 1
    if [[ "$ACTION_YES" -eq 0 ]]; then
        confirm_system_change || { log.warn "已取消"; return 2; }
    fi
    local elev
    elev=$(elevate_cmd)
    [[ -n "$elev" ]] || { log.error "未找到 pkexec/sudo，无法提权修改系统配置"; return 2; }
    exec "$elev" "$0" "$action" "$ACTION_CAT" "$ACTION_NAME" --system --yes
}

cmd_enable_disable() {
    local action="$1"; shift
    parse_task_action "$@" || exit $?
    elevate_task_action "$action"; local elevated=$?
    [[ $elevated -eq 0 ]] && return
    [[ $elevated -eq 2 ]] && exit 1
    task_paths "$ACTION_CAT" "$ACTION_NAME" "$ACTION_SCOPE"
    if [[ "$action" == "disable" ]]; then
        [[ -f "$TASK_ENABLED" ]] || { [[ -f "$TASK_DISABLED" ]] && { log.ok "任务已禁用"; return; }; log.error "找不到任务: $TASK_ENABLED"; exit 1; }
        stop_task_processes "$ACTION_CAT" "$ACTION_NAME" "$TASK_ENABLED"
        mv -- "$TASK_ENABLED" "$TASK_DISABLED"
    else
        [[ -f "$TASK_DISABLED" ]] || { [[ -f "$TASK_ENABLED" ]] && { log.ok "任务已启用"; return; }; log.error "找不到任务: $TASK_DISABLED"; exit 1; }
        mv -- "$TASK_DISABLED" "$TASK_ENABLED"
    fi
    log.ok "已${action} $(tag_scope "$ACTION_SCOPE")/$(tag_category "$ACTION_CAT") $ACTION_NAME"
}

pid_is_active() {
    local pid="$1"
    [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null
}

stop_task_processes() {
    local cat="$1" name="$2" enabled_file="$3" pidfile pid proc cmdline
    pidfile=$(pid_file_for "$cat" "$name")
    if [[ -f "$pidfile" ]]; then
        pid=$(<"$pidfile")
        if pid_is_active "$pid"; then
            kill "$pid" 2>/dev/null || log.warn "无法结束 PID=$pid"
        else
            log.warn "忽略过期 PID 文件: $pidfile"
        fi
        rm -f -- "$pidfile"
    fi
    for proc in /proc/[0-9]*; do
        [[ -r "$proc/cmdline" ]] || continue
        pid=${proc##*/}
        cmdline=$(tr '\0' ' ' < "$proc/cmdline" 2>/dev/null)
        [[ "$cmdline" == *"$enabled_file"* ]] || continue
        [[ "$pid" == "$$" ]] && continue
        kill "$pid" 2>/dev/null || log.warn "无法结束关联 PID=$pid"
    done
}

cmd_start() {
    parse_task_action "$@" || exit $?
    [[ "$ACTION_CAT" != "timer" ]] || { log.error "timer 任务仅支持 enable 和 disable；由守护程序按计划扫描"; exit 2; }
    task_paths "$ACTION_CAT" "$ACTION_NAME" "$ACTION_SCOPE"
    [[ ! -f "$TASK_DISABLED" ]] || { log.error "任务已禁用，请先执行 enable"; exit 1; }
    [[ -f "$TASK_ENABLED" ]] || { log.error "任务不存在: $TASK_ENABLED"; exit 1; }
    local pidfile pid command
    pidfile=$(pid_file_for "$ACTION_CAT" "$ACTION_NAME")
    if [[ -f "$pidfile" ]]; then
        pid=$(<"$pidfile")
        if pid_is_active "$pid"; then log.warn "任务已在运行 (PID=$pid)"; return; fi
        rm -f -- "$pidfile"
    fi
    mkdir -p "$(dirname "$pidfile")"
    case "$ACTION_CAT" in
        edging)
            command=$(bash -c 'source "$1"; printf "%s" "$TARGET_PROCESS"' _ "$TASK_ENABLED")
            [[ -n "$command" ]] || { log.error "edging 配置未定义 TARGET_PROCESS"; exit 1; }
            bash -c 'source "$1"; bash -c "$TARGET_PROCESS" & child=$!; trap "kill \"$child\" 2>/dev/null; exit" TERM INT; wait "$child"' _ "$TASK_ENABLED" </dev/null >/dev/null 2>&1 & pid=$!
            ;;
        slimy|shot)
            bash "$TASK_ENABLED" </dev/null >/dev/null 2>&1 & pid=$!
            ;;
    esac
    [[ -n "$pid" && -d "/proc/$pid" ]] || { log.error "任务启动失败"; exit 1; }
    printf '%s\n' "$pid" > "$pidfile"
    log.ok "已启动 $(tag_category "$ACTION_CAT") $ACTION_NAME (PID=$pid)"
}

cmd_stop() {
    parse_task_action "$@" || exit $?
    [[ "$ACTION_CAT" != "timer" ]] || { log.error "timer 任务仅支持 enable 和 disable；由守护程序按计划扫描"; exit 2; }
    task_paths "$ACTION_CAT" "$ACTION_NAME" "$ACTION_SCOPE"
    local task_file="$TASK_ENABLED"
    [[ -f "$task_file" ]] || task_file="$TASK_DISABLED"
    [[ -f "$task_file" ]] || { log.error "任务不存在: $TASK_ENABLED 或 $TASK_DISABLED"; exit 1; }
    stop_task_processes "$ACTION_CAT" "$ACTION_NAME" "$task_file"
    log.ok "已停止 $(tag_category "$ACTION_CAT") $ACTION_NAME（任务状态未改变）"
    if [[ ( "$ACTION_CAT" == "edging" || "$ACTION_CAT" == "slimy" ) && -f "$TASK_ENABLED" ]]; then
        log.warn "任务仍已启用，K9 守护程序会稍后重新启动它；要永久阻止应 disable。"
    fi
}

cmd_restart() {
    parse_task_action "$@" || exit $?
    [[ "$ACTION_CAT" != "timer" ]] || { log.error "timer 任务仅支持 enable 和 disable；由守护程序按计划扫描"; exit 2; }
    task_paths "$ACTION_CAT" "$ACTION_NAME" "$ACTION_SCOPE"
    [[ ! -f "$TASK_DISABLED" ]] || { log.error "任务已禁用，请先执行 enable"; exit 1; }
    [[ -f "$TASK_ENABLED" ]] || { log.error "任务不存在: $TASK_ENABLED"; exit 1; }
    stop_task_processes "$ACTION_CAT" "$ACTION_NAME" "$TASK_ENABLED"
    "$0" start "$ACTION_CAT" "$ACTION_NAME" $([[ "$ACTION_SCOPE" == system ]] && printf '%s' '--system')
}

# ===================== list 子命令 =====================
cmd_list() {
    local filter_cat="" json=0
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --category) shift; filter_cat="$1";;
            --json) json=1;;
            *) log.error "list: 未知参数 $1"; exit 2;;
        esac
        shift
    done

    [[ "$json" -eq 1 ]] && { cmd_list_json "$filter_cat"; return; }

    # 收集所有条目
    local items=()
    for cat in "${CATEGORIES[@]}"; do
        [[ -n "$filter_cat" && "$cat" != "$filter_cat" ]] && continue
        for scope in user system; do
            local d
            d=$(category_dir "$cat" "$scope")
            [[ -d "$d" ]] || continue
            for f in "$d"/*."$cat" "$d"/*."$cat".disabled; do
                [[ -e "$f" ]] || continue
                local n state
                if [[ "$f" == *.disabled ]]; then
                    n=$(basename "$f" ".${cat}.disabled")
                    state="禁用"
                else
                    n=$(basename "$f" ".${cat}")
                    state="启用"
                fi
                items+=("$cat|$scope|$n|$state|$f")
            done
        done
    done

    if [[ ${#items[@]} -eq 0 ]]; then
        echo
        echo -e "  ${C_DIM}${C_GRAY}暂无任何任务配置。使用 ${C_CYAN}${PROG_NAME} add${C_RESET}${C_DIM}${C_GRAY} 新增。${C_RESET}"
        echo
        return 0
    fi

    echo
    local w=82
    box_title "任务列表" $w

    # ┌──────┬──────────────┬──────┬────┐
    printf "${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-6s${C_RESET}" "分类"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-22s${C_RESET}" "名称"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-6s${C_RESET}" "状态"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-5s${C_RESET}" "来源"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-24s${C_RESET}" "路径"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET}"
    echo

    hline $w

    for item in "${items[@]}"; do
        IFS='|' read -r cat scope name state path <<< "$item"
        printf "${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        tag_category "$cat"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        printf "${C_BOLD}%-22s${C_RESET}" "$name"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        printf "%-6s" "$state"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        tag_scope "$scope"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        printf "${C_GRAY}%-24s${C_RESET}" "$(basename "$(dirname "$path")")/$(basename "$path")"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET}"
        echo
    done

    box_bottom $w
    echo
}

cmd_list_json() {
    local filter_cat="$1"
    echo "{"
    echo "  \"tasks\": ["
    local first=1
    for cat in "${CATEGORIES[@]}"; do
        [[ -n "$filter_cat" && "$cat" != "$filter_cat" ]] && continue
        for scope in user system; do
            local d
            d=$(category_dir "$cat" "$scope")
            [[ -d "$d" ]] || continue
            for f in "$d"/*."$cat" "$d"/*."$cat".disabled; do
                [[ -e "$f" ]] || continue
                local n state
                if [[ "$f" == *.disabled ]]; then n=$(basename "$f" ".${cat}.disabled"); state="disabled"; else n=$(basename "$f" ".${cat}"); state="enabled"; fi
                [[ $first -eq 1 ]] || echo ","
                printf '    {"category":"%s","name":"%s","scope":"%s","state":"%s","path":"%s"}' "$cat" "$n" "$scope" "$state" "$f"
                first=0
            done
        done
    done
    echo ""
    echo "  ]"
    echo "}"
}

# ===================== show 子命令 =====================
cmd_show() {
    local cat="" name="" scope="user"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --system) scope="system";;
            --user)   scope="user";;
            *)
                if [[ -z "$cat" ]]; then cat="$1"
                elif [[ -z "$name" ]]; then name="$1"
                else log.error "show: 多余参数 $1"; exit 2; fi;;
        esac
        shift
    done
    [[ -z "$cat" || -z "$name" ]] && { log.error "show 需要参数 <category> <name>"; exit 2; }

    local d f
    d=$(category_dir "$cat" "$scope")
    f="${d}${name}.${cat}"
    [[ -f "$f" ]] || f="${f}.disabled"
    if [[ ! -f "$f" ]]; then
        log.error "找不到配置: $f"
        exit 1
    fi

    local size
    size=$(stat -c%s "$f" 2>/dev/null || echo "?")
    echo
    box_title "配置内容" 56
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  分类: $(tag_category "$cat")"
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  名称: ${C_BOLD}$name${C_RESET}"
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  来源: $(tag_scope "$scope")"
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  路径: ${C_GRAY}$f${C_RESET}"
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  大小: ${C_GRAY}${size} bytes${C_RESET}"
    box_bottom 56
    echo
    hline 56
    if [[ -x "$f" && "$cat" != "timer" ]]; then
        echo -e "${C_DIM}${C_GRAY}# 可执行脚本${C_RESET}"
    fi
    while IFS= read -r line; do
        printf "  %s\n" "$line"
    done < "$f"
    echo
}

# ===================== add 子命令 =====================
cmd_add() {
    local cat="" name="" target="" schedule="" command="" no_systemd=0 scope="user" yes=0
    if [[ $# -gt 0 && "$1" != --* ]]; then
        cat="$1"; shift
    else
        log.error "add 需要先指定 <category>"; exit 2
    fi

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --name) shift; name="$1";;
            --target) shift; target="$1";;
            --schedule) shift; schedule="$1";;
            --command) shift; command="$1";;
            --no-systemd) no_systemd=1;;
            --system) scope="system";;
            --user)   scope="user";;
            --yes) yes=1;;
            *) log.error "add: 未知参数 $1"; exit 2;;
        esac
        shift
    done

    [[ -z "$name" ]] && { log.error "add 需要 --name"; exit 2; }
    local valid=0
    for c in "${CATEGORIES[@]}"; do [[ "$c" == "$cat" ]] && valid=1 && break; done
    [[ $valid -eq 0 ]] && { log.error "未知分类: $cat （支持: ${CATEGORIES[*]}）"; exit 2; }

    local d f
    d=$(category_dir "$cat" "$scope")
    f="${d}${name}.${cat}"

    if [[ -e "$f" ]]; then
        log.error "已存在同名配置: $f （请先 remove）"
        exit 1
    fi

    # 系统级需要提权
    if [[ "$scope" == "system" ]]; then
        if [[ $yes -eq 0 ]]; then
            confirm_system_change || { log.warn "已取消"; exit 1; }
        fi
        local elev
        elev=$(elevate_cmd)
        if [[ -z "$elev" ]]; then
            log.error "未找到 pkexec/sudo，无法提权修改系统配置"
            exit 1
        fi
        local args=(add "$cat" --name "$name" --system --yes)
        case "$cat" in
            edging) args+=(--target "$target"); [[ $no_systemd -eq 1 ]] && args+=(--no-systemd);;
            timer)  args+=(--schedule "$schedule" --command "$command");;
            slimy|shot) args+=(--command "$command");;
        esac
        exec $elev "$0" "${args[@]}"
    fi

    ensure_dir "$d"

    case "$cat" in
        edging)
            [[ -z "$target" ]] && { log.error "add edging 需要 --target"; exit 2; }
            cat > "$f" <<-EOF
				#!/bin/bash
				# 目标程序路径
				TARGET_PROCESS="$target"
				DONT_RUN_IF_SYSTEMD_EXIST="$([[ $no_systemd -eq 1 ]] && echo 1 || echo "")"
				EOF
            ;;
        timer)
            [[ -z "$schedule" || -z "$command" ]] && { log.error "add timer 需要 --schedule 与 --command"; exit 2; }
            if ! validate_cron "$schedule"; then
                log.error "schedule 格式不正确（应为 5 字段 cron）: $schedule"
                exit 2
            fi
            printf '%s | %s\n' "$schedule" "$command" > "$f"
            ;;
        slimy)
            [[ -z "$command" ]] && { log.error "add slimy 需要 --command"; exit 2; }
            printf '#!/bin/bash\n%s\n' "$command" > "$f"
            chmod +x "$f"
            ;;
        shot)
            [[ -z "$command" ]] && { log.error "add shot 需要 --command"; exit 2; }
            printf '#!/bin/bash\n%s\n' "$command" > "$f"
            chmod +x "$f"
            ;;
    esac

    log.ok "已创建 $(tag_scope "$scope")/$(tag_category "$cat") $name"
}

validate_cron() {
    local s="$1"
    local n
    n=$(echo "$s" | awk '{print NF}')
    [[ "$n" -eq 5 ]] || return 1
    return 0
}

# ===================== remove 子命令 =====================
cmd_remove() {
    parse_task_action "$@" || exit $?
    elevate_task_action remove; local elevated=$?
    [[ $elevated -eq 0 ]] && return
    [[ $elevated -eq 2 ]] && exit 1
    task_paths "$ACTION_CAT" "$ACTION_NAME" "$ACTION_SCOPE"
    local target="$TASK_ENABLED"
    [[ -e "$target" ]] || target="$TASK_DISABLED"
    [[ -e "$target" ]] || { log.error "不存在: $TASK_ENABLED 或 $TASK_DISABLED"; exit 1; }
    stop_task_processes "$ACTION_CAT" "$ACTION_NAME" "$target"
    rm -f -- "$TASK_ENABLED" "$TASK_DISABLED"
    log.ok "已删除 $(tag_scope "$ACTION_SCOPE")/$(tag_category "$ACTION_CAT") $ACTION_NAME"
}

# ===================== import 子命令 =====================
cmd_import() {
    local cat="" src="" name="" scope="user" yes=0
    if [[ $# -gt 0 && "$1" != --* ]]; then
        cat="$1"; shift
    else
        log.error "import 需要先指定 <category>"; exit 2
    fi
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --name) shift; name="$1";;
            --system) scope="system";;
            --user)   scope="user";;
            --yes) yes=1;;
            *)
                if [[ -z "$src" ]]; then src="$1"
                else log.error "import: 多余参数 $1"; exit 2; fi;;
        esac
        shift
    done
    [[ -z "$src" ]] && { log.error "import 需要 <src-file>"; exit 2; }
    [[ ! -f "$src" ]] && { log.error "源文件不存在: $src"; exit 1; }

    local valid=0
    for c in "${CATEGORIES[@]}"; do [[ "$c" == "$cat" ]] && valid=1 && break; done
    [[ $valid -eq 0 ]] && { log.error "未知分类: $cat"; exit 2; }

    if [[ -z "$name" ]]; then
        name=$(basename "$src")
        name="${name%.slimy}"; name="${name%.shot}"
        name="${name%.edging}"; name="${name%.timer}"
        name="${name%.sh}"; name="${name%.bash}"
    fi

    local d f
    d=$(category_dir "$cat" "$scope")
    f="${d}${name}.${cat}"
    if [[ -e "$f" ]]; then
        log.error "已存在同名配置: $f"
        exit 1
    fi

    if [[ "$scope" == "system" ]]; then
        if [[ $yes -eq 0 ]]; then
            confirm_system_change || { log.warn "已取消"; exit 1; }
        fi
        local elev
        elev=$(elevate_cmd)
        if [[ -z "$elev" ]]; then
            log.error "未找到 pkexec/sudo，无法提权修改系统配置"
            exit 1
        fi
        local tmp
        tmp=$(mktemp "/tmp/k9-imp-XXXXXX.${cat}")
        cp "$src" "$tmp"
        exec $elev "$0" _import_as_root "$cat" "$tmp" "$name" --system --yes
    fi

    ensure_dir "$d"
    cp "$src" "$f"
    log.ok "已导入 $(tag_scope "$scope")/$(tag_category "$cat") $name"
}

cmd__import_as_root() {
    local cat="$1" src="$2" name="$3"; shift 3
    local scope="system"
    while [[ $# -gt 0 ]]; do
        case "$1" in --system|--yes) ;; esac
        shift
    done
    local d f
    d=$(category_dir "$cat" "$scope")
    f="${d}${name}.${cat}"
    ensure_dir "$d"
    cp "$src" "$f"
    rm -f "$src"
    log.ok "已导入 $(tag_scope "$scope")/$(tag_category "$cat") $name"
}

# ===================== status 子命令 =====================
cmd_status() {
    local cat="" name="" scope="user"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --system) scope="system";;
            --user) scope="user";;
            *)
                if [[ -z "$cat" ]]; then cat="$1"
                elif [[ -z "$name" ]]; then name="$1"
                else log.error "status: 多余参数 $1"; return 2; fi;;
        esac
        shift
    done
    if [[ -n "$cat" || -n "$name" ]]; then
        [[ -n "$cat" && -n "$name" ]] || { log.error "status 需要同时提供 <category> <name>"; return 2; }
        valid_category "$cat" || { log.error "未知分类: $cat"; return 2; }
        task_paths "$cat" "$name" "$scope"
        local f="$TASK_ENABLED" state="已启用" pidfile pid
        if [[ -f "$TASK_DISABLED" ]]; then f="$TASK_DISABLED"; state="已禁用"; fi
        [[ -f "$f" ]] || { log.error "找不到 ${scope} 任务: $cat / $name"; return 1; }
        echo "任务: $cat / $name"
        echo "范围: $scope"
        echo "状态: $state"
        echo "路径: $f"
        if [[ "$cat" == "timer" ]]; then
            echo "运行状态: 由守护程序按计划扫描"
        else
            pidfile=$(pid_file_for "$cat" "$name")
            if [[ -f "$pidfile" ]]; then
                pid=$(<"$pidfile")
                if pid_is_active "$pid"; then echo "运行状态: 运行中 (PID=$pid)"; else echo "运行状态: 未运行（PID 文件已过期）"; fi
            else
                echo "运行状态: 未运行"
            fi
        fi
        return 0
    fi
    echo
    box_title "运行状态" 50

    # 检查守护进程
    local pid lockfile line
    lockfile="/tmp/GXDE/gxde-k9/$UID/gxde-k9-daemon.lock"
    if [[ -f "$lockfile" ]]; then
        pid=$(cat "$lockfile" 2>/dev/null)
        if [[ -n "$pid" && -e "/proc/$pid" ]]; then
            local uptime
            uptime=$(ps -o etime= -p "$pid" 2>/dev/null | xargs)
            echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  K9 守护进程: ${C_GREEN}${C_BOLD}运行中${C_RESET}  PID=$pid  运行时间=$uptime"
        else
            echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  K9 守护进程: ${C_RED}${C_BOLD}未运行${C_RESET}  (存在过期锁文件)"
            rm -f "$lockfile" 2>/dev/null
        fi
    else
        echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  K9 守护进程: ${C_RED}${C_BOLD}未运行${C_RESET}"
    fi

    # 列出各分类数目
    local total=0
    for cat in "${CATEGORIES[@]}"; do
        local cnt=0
        for scope in user system; do
            local d
            d=$(category_dir "$cat" "$scope")
            [[ -d "$d" ]] || continue
            for f in "$d"/*."$cat"; do [[ -e "$f" ]] && cnt=$((cnt+1)); done
        done
        total=$((total+cnt))
        echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  $(tag_category "$cat")  ${C_GRAY}${cnt} 个配置${C_RESET}"
    done
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  ${C_DIM}配置总数: ${C_BOLD}${total}${C_RESET}"

    # 日志统计
    local log_count=0 log_size=0
    if [[ -d "$LOG_DIR_USER" ]]; then
        while IFS= read -r f; do
            log_count=$((log_count+1))
            local sz
            sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
            log_size=$((log_size+sz))
        done < <(find "$LOG_DIR_USER" -name '*.log' -type f 2>/dev/null)
    fi
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  ${C_DIM}日志文件: ${log_count} 个, ${log_size} bytes${C_RESET}"

    box_bottom 50
    echo
}

# ===================== logs 子命令 =====================
cmd_logs() {
    local sub="${1:-}"
    [[ -z "$sub" ]] && { log.error "logs 需要子命令: list|show|export|clear"; exit 2; }
    shift
    case "$sub" in
        list)   logs_list "$@";;
        show)   logs_show "$@";;
        export) logs_export "$@";;
        clear)  logs_clear "$@";;
        *) log.error "未知 logs 子命令: $sub"; exit 2;;
    esac
}

logs_list() {
    [[ ! -d "$LOG_DIR_USER" ]] && {
        echo
        echo -e "  ${C_DIM}${C_GRAY}暂无日志。K9 守护进程尚未运行，或未产生日志。${C_RESET}"
        echo
        return
    }

    local items=()
    for cat_dir in "$LOG_DIR_USER"*/; do
        [[ -d "$cat_dir" ]] || continue
        local cat
        cat=$(basename "$cat_dir")
        for f in "$cat_dir"*.log; do
            [[ -e "$f" ]] || continue
            local n sz
            n=$(basename "$f" .log)
            sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
            items+=("$cat|$n|$f|$sz")
        done
    done

    [[ ${#items[@]} -eq 0 ]] && {
        echo
        echo -e "  ${C_DIM}${C_GRAY}暂无任务日志。${C_RESET}"
        echo
        return
    }

    echo
    local w=62
    box_title "任务日志" $w

    # 表头
    printf "${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-6s${C_RESET}" "分类"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-22s${C_RESET}" "任务"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%6s${C_RESET}" "大小"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
    printf "${C_BOLD}%-20s${C_RESET}" "路径"
    printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET}"
    echo

    hline $w

    for item in "${items[@]}"; do
        IFS='|' read -r cat name path size <<< "$item"
        printf "${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        tag_category "$cat"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        printf "${C_BOLD}%-22s${C_RESET}" "$name"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        printf "${C_GRAY}%6s${C_RESET} " "$(numfmt_size "$size")"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET} "
        printf "${C_GRAY}%-20s${C_RESET}" "$(basename "$path")"
        printf " ${C_GRAY}${C_DIM}${T_MID}${C_RESET}"
        echo
    done

    box_bottom $w
    echo
}

logs_show() {
    local cat="${1:-}" name="${2:-}"
    [[ -z "$cat" || -z "$name" ]] && { log.error "logs show 需要 <category> <name>"; exit 2; }
    local f="${LOG_DIR_USER}${cat}/${name}.log"
    if [[ ! -f "$f" ]]; then
        log.error "找不到日志: $f"
        exit 1
    fi
    local sz
    sz=$(stat -c%s "$f" 2>/dev/null || echo "?")
    echo
    box_title "日志: $cat/$name" 56
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  路径: ${C_GRAY}$f${C_RESET}"
    echo -e "${C_GRAY}${C_DIM}${T_MID}${C_RESET}  大小: ${C_GRAY}$(numfmt_size "$sz")${C_RESET}"
    box_bottom 56
    echo
    hline 56
    while IFS= read -r line; do
        printf "  %s\n" "$line"
    done < "$f"
    echo
}

logs_export() {
    local cat="${1:-}" name="${2:-}" dest="${3:-}"
    [[ -z "$cat" || -z "$name" || -z "$dest" ]] && { log.error "logs export 需要 <category> <name> <dest>"; exit 2; }
    local f="${LOG_DIR_USER}${cat}/${name}.log"
    if [[ ! -f "$f" ]]; then
        log.error "找不到日志: $f"
        exit 1
    fi
    cp "$f" "$dest"
    log.ok "已导出 $f -> $dest"
}

logs_clear() {
    [[ ! -d "$LOG_DIR_USER" ]] && { log.warn "日志目录不存在: $LOG_DIR_USER"; return; }
    rm -rf "${LOG_DIR_USER:?}/"*/  2>/dev/null
    find "$LOG_DIR_USER" -type f -name '*.log' -delete 2>/dev/null
    log.ok "已清空所有日志"
}

# ===================== TUI 选择器 =====================
# pick_category: stdout -> category; rc=1 取消
pick_category() {
    local label="${1:-请选择分类}"
    local items=()
    for c in "${CATEGORIES[@]}"; do
        local desc=""
        case "$c" in
            slimy)  desc="周期执行（每 5 秒）";;
            timer)  desc="crontab 定时任务";;
            shot)   desc="启动时一次性执行";;
            edging) desc="服务保活（拉起进程）";;
        esac
        items+=("$c" "$desc")
    done
    tui_menu "$label" "↑↓ 移动  回车 确认  ESC 取消" "${items[@]}"
}

# pick_scope: stdout -> user|system; rc=1 取消
pick_scope() {
    local label="${1:-选择位置}"
    tui_menu "$label" "选择配置存放位置" \
        "user"   "用户侧（$BASE_DIR_USER，无需提权）" \
        "system" "系统侧（/usr/share/gxde-k9，需提权）"
}

# pick_task: stdout -> task name; rc=1 取消
# 参数: <category> <scope>
pick_task() {
    local cat="$1" scope="${2:-user}"
    local d
    d=$(category_dir "$cat" "$scope")
    [[ -d "$d" ]] || { tui_msgbox "错误" "目录不存在: $d"; return 1; }

    local items=()
    local f n sz mtime state
    for f in "$d"/*."$cat" "$d"/*."$cat".disabled; do
        [[ -f "$f" ]] || continue
        if [[ "$f" == *.disabled ]]; then n=$(basename "$f" ".${cat}.disabled"); state="已禁用"; else n=$(basename "$f" ."$cat"); state="已启用"; fi
        sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
        mtime=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
        items+=("$n" "$state  $(numfmt_size "$sz")  |  $mtime")
    done

    if [[ ${#items[@]} -eq 0 ]]; then
        tui_msgbox "无任务" "当前 $scope 的 $cat 没有可选项"
        return 1
    fi
    tui_menu "选择 $cat 任务" "来源: $scope  |  共 $(( ${#items[@]} / 2 )) 项" "${items[@]}"
}

# pick_task_multi: stdout -> 选中任务名（换行分隔）; rc=1 取消
# 参数: <category> <scope>
pick_task_multi() {
    local cat="$1" scope="${2:-user}"
    local d
    d=$(category_dir "$cat" "$scope")
    [[ -d "$d" ]] || { tui_msgbox "错误" "目录不存在: $d"; return 1; }

    local items=()
    local f n mtime state
    for f in "$d"/*."$cat" "$d"/*."$cat".disabled; do
        [[ -f "$f" ]] || continue
        if [[ "$f" == *.disabled ]]; then n=$(basename "$f" ".${cat}.disabled"); state="已禁用"; else n=$(basename "$f" ."$cat"); state="已启用"; fi
        mtime=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
        items+=("$n" "$state  $mtime" "off")
    done

    if [[ ${#items[@]} -eq 0 ]]; then
        tui_msgbox "无任务" "当前 $scope 的 $cat 没有可选项"
        return 1
    fi
    tui_checklist "批量选择 $cat 任务" "空格 切换选中  回车 确认  ESC 取消\n来源: $scope" "${items[@]}"
}

# pick_log_task: 从 logs 目录选择任务; stdout -> "cat|name"; rc=1 取消
# 用 | 分隔以便后续拆分（任务名本身可能含 / 或空格）
pick_log_task() {
    [[ -d "$LOG_DIR_USER" ]] || { tui_msgbox "无日志" "日志目录不存在:\n$LOG_DIR_USER"; return 1; }

    local items=()
    local cat_dir cat f n sz mtime
    for cat_dir in "$LOG_DIR_USER"*/; do
        [[ -d "$cat_dir" ]] || continue
        cat=$(basename "$cat_dir")
        for f in "$cat_dir"*.log; do
            [[ -f "$f" ]] || continue
            n=$(basename "$f" .log)
            sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
            mtime=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
            items+=("${cat}|${n}" "$(numfmt_size "$sz")  ${mtime}  ${cat}/${n}")
        done
    done

    if [[ ${#items[@]} -eq 0 ]]; then
        tui_msgbox "无日志" "尚未产生任何任务日志"
        return 1
    fi

    tui_menu "选择日志" "共 $(( ${#items[@]} / 2 )) 项  ↑↓ 移动  回车 查看  ESC 取消" "${items[@]}"
}

# ---------------------------------------------------------------
#  交互式新增
# ---------------------------------------------------------------
interactive_add() {
    local cat scope name
    cat=$(pick_category "新增配置 - 选择分类") || return
    scope=$(pick_scope "新增配置 - 选择位置") || return

    name=$(tui_inputbox "新增 $cat - 任务名称" "请输入任务名称（不留空）") || return
    [[ -z "$name" ]] && { tui_msgbox "提示" "名称不能为空"; return; }

    case "$cat" in
        edging)
            local target
            target=$(tui_inputbox "edging - TARGET_PROCESS" "请输入要拉起的命令") || return
            [[ -z "$target" ]] && { tui_msgbox "提示" "target 不能为空"; return; }
            local args=(add edging --name "$name" --target "$target")
            # 若 systemd 已存在则跳过
            if tui_yesno "edging - systemd 检测" "若环境已存在 systemd，是否跳过启动该 edging 任务？\n（常见于容器/proot 环境）"; then
                args+=(--no-systemd)
            fi
            [[ "$scope" == "system" ]] && args+=(--system)
            "$0" "${args[@]}"
            ;;
        timer)
            local schedule command
            schedule=$(tui_inputbox "timer - cron 表达式" \
                "请输入 5 字段 cron 表达式\n\n示例：\n  * * * * *        每分钟\n  */5 * * * *      每 5 分钟\n  0 9 * * 1-5      工作日 9 点\n  0 */2 * * *      每 2 小时") || return
            [[ -z "$schedule" ]] && { tui_msgbox "提示" "schedule 不能为空"; return; }
            if ! validate_cron "$schedule"; then
                tui_msgbox "格式错误" "schedule 应为 5 字段 cron 表达式\n输入: $schedule"
                return
            fi
            command=$(tui_inputbox "timer - 执行命令" "请输入定时执行的命令") || return
            [[ -z "$command" ]] && { tui_msgbox "提示" "command 不能为空"; return; }
            local args=(add timer --name "$name" --schedule "$schedule" --command "$command")
            [[ "$scope" == "system" ]] && args+=(--system)
            "$0" "${args[@]}"
            ;;
        slimy|shot)
            local command
            command=$(tui_inputbox "$cat - 执行命令" "请输入要执行的命令\n（slimy 会每 5 秒触发一次；shot 仅启动时执行一次）") || return
            [[ -z "$command" ]] && { tui_msgbox "提示" "command 不能为空"; return; }
            local args=(add "$cat" --name "$name" --command "$command")
            [[ "$scope" == "system" ]] && args+=(--system)
            "$0" "${args[@]}"
            ;;
    esac

    tui_msgbox "完成" "操作结束，按回车返回主菜单"
}

# ---------------------------------------------------------------
#  交互式导入
# ---------------------------------------------------------------
interactive_import() {
    local cat src name scope
    cat=$(pick_category "导入脚本 - 选择分类") || return

    src=$(tui_inputbox "导入 $cat - 文件路径" "请输入要导入的脚本文件路径") || return
    [[ ! -f "$src" ]] && { tui_msgbox "错误" "文件不存在:\n$src"; return; }

    name=$(tui_inputbox "导入 $cat - 任务名称" "留空则使用文件名") || true
    [[ -z "$name" ]] && name=""
    scope=$(pick_scope "导入 $cat - 选择位置") || return

    local args=(import "$cat" "$src")
    [[ -n "$name" ]] && args+=(--name "$name")
    [[ "$scope" == "system" ]] && args+=(--system)
    "$0" "${args[@]}"

    tui_msgbox "完成" "操作结束，按回车返回主菜单"
}

# ---------------------------------------------------------------
#  交互式删除（单删/批删）
# ---------------------------------------------------------------
interactive_remove() {
    local cat scope
    cat=$(pick_category "删除配置 - 选择分类") || return
    scope=$(pick_scope "删除 $cat - 选择位置") || return

    # 选择模式
    local mode
    mode=$(tui_menu "删除 $cat - 选择模式" "请选择删除模式" \
        "single" "删除单个任务" \
        "batch"  "批量删除（多选）") || return

    if [[ "$mode" == "batch" ]]; then
        local selected
        selected=$(pick_task_multi "$cat" "$scope") || return
        [[ -z "$selected" ]] && { tui_msgbox "提示" "未选中任何任务"; return; }

        # 统计并确认
        local count=0 list=""
        local n
        while IFS= read -r n; do
            [[ -z "$n" ]] && continue
            count=$((count+1))
            list+="  • $n\n"
        done <<< "$selected"

        if ! tui_yesno "确认批量删除" "即将删除 $count 个 $cat 任务：\n$list\n此操作不可撤销，是否继续？"; then
            tui_msgbox "已取消" "用户取消操作"
            return
        fi
        local rc=0
        while IFS= read -r n; do
            [[ -z "$n" ]] && continue
            local args=(remove "$cat" "$n")
            [[ "$scope" == "system" ]] && args+=(--system)
            "$0" "${args[@]}" || rc=$?
        done <<< "$selected"
        tui_msgbox "完成" "批量删除已结束（最后一次退出码 $rc）\n按回车返回主菜单"
        return
    fi

    # 单删
    local name
    name=$(pick_task "$cat" "$scope") || return
    if tui_yesno "确认删除" "即将删除：\n  $cat / $name  ($scope)\n\n此操作不可撤销，是否继续？"; then
        local args=(remove "$cat" "$name")
        [[ "$scope" == "system" ]] && args+=(--system)
        "$0" "${args[@]}"
    else
        tui_msgbox "已取消" "用户取消操作"
    fi
    tui_msgbox "完成" "操作结束，按回车返回主菜单"
}

# ---------------------------------------------------------------
#  交互式查看配置
# ---------------------------------------------------------------
# 展示单个任务的完整信息（元信息 + 文件内容），用滚动 textbox 放大显示
_show_task_detail() {
    local cat="$1" scope="$2" name="$3" f="$4" state="已启用"
    [[ "$f" == *.disabled ]] && state="已禁用"
    if [[ ! -f "$f" ]]; then
        tui_msgbox "错误" "找不到配置:\n$f"
        return 1
    fi

    local tmp
    tmp=$(mktemp /tmp/k9-show-XXXXXX.txt)
    {
        echo "================== 配置信息 =================="
        echo "分类:   $cat"
        echo "名称:   $name"
        echo "来源:   $scope"
        echo "状态:   $state"
        echo "路径:   $f"
        echo "大小:   $(stat -c%s "$f" 2>/dev/null || echo '?') bytes"
        echo "修改:   $(stat -c%y "$f" 2>/dev/null | cut -d. -f1)"
        [[ -x "$f" && "$cat" != "timer" ]] && echo "可执行: 是"
        echo "================== 文件内容 =================="
        cat "$f"
        echo "==============================================="
    } > "$tmp"
    tui_textbox "$cat / $name ($scope)" "$tmp"
    rm -f "$tmp"
}

interactive_show() {
    local cat scope name
    cat=$(pick_category "查看配置 - 选择分类") || return
    scope=$(pick_scope "查看 $cat - 选择来源") || return
    name=$(pick_task "$cat" "$scope") || return

    local d f
    d=$(category_dir "$cat" "$scope")
    f="${d}${name}.${cat}"
    _show_task_detail "$cat" "$scope" "$name" "$f"
}

# ---------------------------------------------------------------
#  交互式列表 —— 每条任务一项，上下选中后放大查看详情
# ---------------------------------------------------------------
interactive_list() {
    local mode filter_cat=""
    mode=$(tui_menu "列出任务" "请选择列出方式" "all" "列出所有分类" "filter" "按分类过滤") || return
    [[ "$mode" == "filter" ]] && filter_cat=$(pick_category "选择分类") || true
    while true; do
        local items=() cat scope d f n state sz mtime sel rest s_cat s_scope s_name s_file
        for cat in "${CATEGORIES[@]}"; do
            [[ -n "$filter_cat" && "$cat" != "$filter_cat" ]] && continue
            for scope in user system; do
                d=$(category_dir "$cat" "$scope")
                [[ -d "$d" ]] || continue
                for f in "$d"/*."$cat" "$d"/*."$cat".disabled; do
                    [[ -f "$f" ]] || continue
                    if [[ "$f" == *.disabled ]]; then n=$(basename "$f" ".${cat}.disabled"); state="已禁用"; else n=$(basename "$f" ".${cat}"); state="已启用"; fi
                    sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
                    mtime=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
                    items+=("${cat}|${scope}|${n}" "${state}  ${scope}  $(numfmt_size "$sz")  ${mtime}")
                done
            done
        done
        [[ ${#items[@]} -gt 0 ]] || { tui_msgbox "无任务" "当前没有可显示的任务配置"; return; }
        sel=$(tui_menu "任务列表" "共 $(( ${#items[@]} / 2 )) 项；选择任务查看详情和操作" "${items[@]}") || return
        s_cat="${sel%%|*}"; rest="${sel#*|}"; s_scope="${rest%%|*}"; s_name="${rest#*|}"
        while true; do
            task_paths "$s_cat" "$s_name" "$s_scope"
            [[ -f "$TASK_ENABLED" ]] && s_file="$TASK_ENABLED" || s_file="$TASK_DISABLED"
            local action menu_items=("view" "查看配置内容" "status" "查看任务状态" "logs" "查看任务日志")
            if [[ "$s_cat" != "timer" ]]; then
                menu_items+=("start" "立即启动一次" "restart" "停止后立即启动" "stop" "停止关联进程（保留状态）")
            fi
            if [[ -f "$TASK_ENABLED" ]]; then
                menu_items+=("disable" "仅禁用（改后缀）")
            else
                menu_items+=("enable" "仅启用（改后缀）")
            fi
            menu_items+=("remove" "停止并删除" "back" "返回任务列表")
            action=$(tui_menu "$s_cat / $s_name" "当前状态：$([[ -f "$TASK_ENABLED" ]] && echo 已启用 || echo 已禁用)" "${menu_items[@]}") || break
            [[ "$action" == "back" ]] && break
            if [[ "$action" == "view" ]]; then
                _show_task_detail "$s_cat" "$s_scope" "$s_name" "$s_file"
                continue
            fi
            if [[ "$action" == "status" ]]; then
                local status_args=(status "$s_cat" "$s_name")
                [[ "$s_scope" == "system" ]] && status_args+=(--system)
                "$0" "${status_args[@]}" | tui_show_stdin "任务状态"
                continue
            fi
            if [[ "$action" == "logs" ]]; then
                _show_log_detail "$s_cat" "$s_name" "${LOG_DIR_USER}${s_cat}/${s_name}.log"
                continue
            fi
            if [[ "$action" == "remove" ]] && ! tui_yesno "确认删除" "删除 $s_cat / $s_name？此操作不可撤销。"; then continue; fi
            local args=("$action" "$s_cat" "$s_name")
            [[ "$s_scope" == "system" ]] && args+=(--system)
            "$0" "${args[@]}"
            [[ "$action" == "remove" ]] && break
        done
    done
}

# ---------------------------------------------------------------
#  交互式日志管理
#  与任务列表同一逻辑：列出所有日志，上下选中后用 textbox 放大查看。
# ---------------------------------------------------------------

# 放大展示某个日志文件（元信息 + 内容）
_show_log_detail() {
    local cat="$1" name="$2" f="$3"
    if [[ ! -f "$f" ]]; then
        tui_msgbox "错误" "找不到日志:\n$f"
        return 1
    fi

    local tmp
    tmp=$(mktemp /tmp/k9-log-XXXXXX.txt)
    {
        echo "================== 日志信息 =================="
        echo "分类:   $cat"
        echo "任务:   $name"
        echo "路径:   $f"
        echo "大小:   $(stat -c%s "$f" 2>/dev/null || echo '?') bytes"
        echo "修改:   $(stat -c%y "$f" 2>/dev/null | cut -d. -f1)"
        echo "================== 日志内容 =================="
        cat "$f"
        echo "==============================================="
    } > "$tmp"
    tui_textbox "日志: $cat/$name" "$tmp"
    rm -f "$tmp"
}

# 收集所有日志、上下选中查看（与 interactive_list 的循环结构一致）
_logs_browse() {
    [[ -d "$LOG_DIR_USER" ]] || { tui_msgbox "无日志" "日志目录不存在:\n$LOG_DIR_USER"; return 1; }

    local items=()
    local cat_dir cat f n sz mtime
    for cat_dir in "$LOG_DIR_USER"*/; do
        [[ -d "$cat_dir" ]] || continue
        cat=$(basename "$cat_dir")
        for f in "$cat_dir"*.log; do
            [[ -f "$f" ]] || continue
            n=$(basename "$f" .log)
            sz=$(stat -c%s "$f" 2>/dev/null || echo 0)
            mtime=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
            items+=("${cat}|${n}" "$(numfmt_size "$sz")  ${mtime}  ${cat}/${n}")
        done
    done

    if [[ ${#items[@]} -eq 0 ]]; then
        tui_msgbox "无日志" "尚未产生任何任务日志"
        return
    fi

    while true; do
        local sel
        sel=$(tui_menu "任务日志" "共 $(( ${#items[@]} / 2 )) 项  ↑↓ 选择  回车查看详情  ESC 返回" "${items[@]}") || return

        # 解析 sel: "cat|name"
        local s_cat s_name s_file
        s_cat="${sel%%|*}"
        s_name="${sel#*|}"
        s_file="${LOG_DIR_USER}${s_cat}/${s_name}.log"

        _show_log_detail "$s_cat" "$s_name" "$s_file"
    done
}

interactive_logs() {
    while true; do
        local sub
        sub=$(tui_menu "日志管理" "请选择操作" \
            "browse" "浏览/查看日志" \
            "export" "导出日志" \
            "clear"  "清空所有日志" \
            "back"   "返回主菜单") || return

        case "$sub" in
            browse)
                _logs_browse
                ;;
            export)
                local sel cat name dest
                sel=$(pick_log_task) || continue
                # sel 形如 "cat|name"
                cat="${sel%%|*}"
                name="${sel#*|}"
                dest=$(tui_inputbox "导出路径" "请输入导出目标文件路径" "$HOME/${cat}-${name}.log") || continue
                "$0" logs export "$cat" "$name" "$dest"
                tui_msgbox "完成" "导出完成，按回车返回"
                ;;
            clear)
                if tui_yesno "确认清空" "即将清空所有任务日志，此操作不可撤销，是否继续？"; then
                    "$0" logs clear
                    tui_msgbox "完成" "已清空所有日志，按回车返回"
                else
                    tui_msgbox "已取消" "用户取消操作"
                fi
                ;;
            back)
                return
                ;;
        esac
    done
}

# ---------------------------------------------------------------
#  主菜单
# ---------------------------------------------------------------
interactive_menu() {
    while true; do
        _refresh_backtitle
        local choice
        choice=$(tui_menu "GXDE K9 Chocker — 主菜单" "↑↓ 移动  回车 确认  ESC 退出" \
            "list"    "浏览任务、详情与启停操作" \
            "add"     "新增配置" \
            "import"  "从外部导入脚本" \
            "remove"  "删除配置（支持批量）" \
            "logs"    "日志管理" \
            "status"  "查看运行状态" \
            "quit"    "退出") || {
            # ESC 或取消 = 退出
            echo -e "${C_GREEN}${C_BOLD}再见！${C_RESET}"
            exit 0
        }

        case "$choice" in
            list)    interactive_list;;
            add)     interactive_add;;
            import)  interactive_import;;
            remove)  interactive_remove;;
            logs)    interactive_logs;;
            status)  "$0" status | tui_show_stdin "运行状态";;
            quit)
                echo -e "${C_GREEN}${C_BOLD}再见！${C_RESET}"
                exit 0
                ;;
        esac
    done
}

# ===================== 入口 =====================
main() {
    detect_tui_backend

    if [[ $# -eq 0 ]]; then
        require_tui
        interactive_menu
        return
    fi

    case "$1" in
        list)     shift; cmd_list "$@";;
        add)      shift; cmd_add "$@";;
        start)    shift; cmd_start "$@";;
        restart)  shift; cmd_restart "$@";;
        stop)     shift; cmd_stop "$@";;
        enable)   shift; cmd_enable_disable enable "$@";;
        disable)  shift; cmd_enable_disable disable "$@";;
        remove)   shift; cmd_remove "$@";;
        import)   shift; cmd_import "$@";;
        _import_as_root) shift; cmd__import_as_root "$@";;
        logs)     shift; cmd_logs "$@";;
        status)   shift; cmd_status "$@";;
        help|-h|--help) print_help;;
        version|--version|-v) echo "${PROG_NAME} v${VERSION}";;
        *)
            log.error "未知命令: $1"
            print_help
            exit 2;;
    esac
}

main "$@"
