#!/usr/bin/env bash
set -euo pipefail

NGC_ROOT="${NGC_ROOT:-}"
NGC_NGINX_BIN="${NGC_NGINX_BIN:-}"
NGC_NGINX_CONF="${NGC_NGINX_CONF:-}"
NGC_STATE_DIR="${NGC_STATE_DIR:-${XDG_STATE_HOME:-${HOME:-$PWD}/.local/state}/nginx-config-control}"
NGC_CONFIG_FILE="${NGC_CONFIG_FILE:-${XDG_CONFIG_HOME:-${HOME:-$PWD}/.config}/nginx-config-control/config}"
NGC_LOCK_FILE="${NGC_LOCK_FILE:-/run/ngc.lock}"
NGC_VERSION="${NGC_VERSION:-v1.0.2}"
NGC_PROJECT="nginx-config-control"

FORCE=0
YES_FLAG=0
CONF_DEFAULT=0
NGC_ROOT_REAL=""
LOCK_HELD=0
LOCK_MODE=""
LOCK_DIR=""
TMP_DIR=""
EDIT_TMP=""
STATE_TMP=""
DL_INSECURE=0
EDIT_CHANGED=0
EDIT_VALID=1
TAB=$(printf '\t')
TTY_SAVED=""
TUI_ACTIVE=0

SCRIPT_PATH=$0
case "$SCRIPT_PATH" in
  */*) ;;
  *) SCRIPT_PATH=$(command -v -- "$SCRIPT_PATH" || true) ;;
esac
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)
NGC_PREFIX="${NGC_PREFIX:-$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)}"

info() {
  printf '%s\n' "$*"
}

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

die() {
  msg=$1
  code=${2:-1}
  printf 'ngc: %s\n' "$msg" >&2
  exit "$code"
}

require_root() {
  if [ "$(id -u)" -ne 0 ]; then
    die "需要 root 权限，请使用 root 或 sudo 运行" 3
  fi
}

require_force() {
  if [ "$FORCE" -ne 1 ]; then
    die "目标已存在冲突，使用 --force 覆盖" 5
  fi
}

cleanup() {
  if [ -n "${TTY_SAVED:-}" ]; then
    stty "$TTY_SAVED" 2>/dev/null || true
    TTY_SAVED=""
  fi
  if [ "${TUI_ACTIVE:-0}" -eq 1 ]; then
    printf '\033[?25h' 2>/dev/null || true
    TUI_ACTIVE=0
  fi
  if [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then
    rm -rf -- "$TMP_DIR"
  fi
  if [ -n "${EDIT_TMP:-}" ] && [ -f "$EDIT_TMP" ]; then
    rm -f -- "$EDIT_TMP"
  fi
  if [ -n "${STATE_TMP:-}" ] && [ -f "$STATE_TMP" ]; then
    rm -f -- "$STATE_TMP"
  fi
  release_lock
}

trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
if [ -n "${ZSH_VERSION:-}" ]; then
  trap cleanup ZERR
fi

release_lock() {
  if [ "${LOCK_HELD:-0}" -ne 1 ]; then
    return 0
  fi
  if [ "$LOCK_MODE" = "flock" ]; then
    flock -u 9 2>/dev/null || true
    exec 9>&- 2>/dev/null || true
  elif [ "$LOCK_MODE" = "mkdir" ]; then
    rmdir -- "$LOCK_DIR" 2>/dev/null || true
  fi
  LOCK_HELD=0
  LOCK_MODE=""
  LOCK_DIR=""
}

acquire_lock() {
  if [ "$LOCK_HELD" -eq 1 ]; then
    return 0
  fi
  lock_file=$NGC_LOCK_FILE
  lock_parent=$(dirname -- "$lock_file")
  if [ ! -d "$lock_parent" ] || [ ! -w "$lock_parent" ]; then
    mkdir -p -- "$NGC_STATE_DIR" || die "无法创建状态目录: $NGC_STATE_DIR" 9
    lock_file="$NGC_STATE_DIR/ngc.lock"
  fi
  if command -v flock >/dev/null 2>&1; then
    exec 9>>"$lock_file" || die "无法打开锁文件: $lock_file" 9
    if ! flock -w 10 9; then
      exec 9>&- 2>/dev/null || true
      die "获取锁失败（超时）: $lock_file" 9
    fi
    LOCK_MODE=flock
    LOCK_HELD=1
  else
    LOCK_DIR="$lock_file.d"
    tries=0
    while ! mkdir -- "$LOCK_DIR" 2>/dev/null; do
      tries=$((tries + 1))
      if [ "$tries" -ge 20 ]; then
        die "获取锁失败（超时）: $LOCK_DIR" 9
      fi
      sleep 0.5
    done
    LOCK_MODE=mkdir
    LOCK_HELD=1
  fi
}

config_get() {
  key=$1
  if [ ! -f "$NGC_CONFIG_FILE" ] || [ ! -r "$NGC_CONFIG_FILE" ]; then
    return 0
  fi
  sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*(.*)$/\\1/p" "$NGC_CONFIG_FILE" 2>/dev/null | tail -n 1
}

sanitize_config_value() {
  value=$1
  value=$(printf '%s\n' "$value" | sed -E 's/[[:space:]]#.*$//')
  value=$(printf '%s\n' "$value" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
  case "$value" in
    \"*\") value=${value#\"}; value=${value%\"} ;;
    \'*\') value=${value#\'}; value=${value%\'} ;;
  esac
  printf '%s' "$value"
}

load_config() {
  if [ -z "$NGC_ROOT" ]; then
    NGC_ROOT=$(sanitize_config_value "$(config_get NGC_ROOT)")
  fi
  if [ -z "$NGC_NGINX_BIN" ]; then
    NGC_NGINX_BIN=$(sanitize_config_value "$(config_get NGC_NGINX_BIN)")
  fi
  if [ -z "$NGC_NGINX_CONF" ]; then
    NGC_NGINX_CONF=$(sanitize_config_value "$(config_get NGC_NGINX_CONF)")
  fi
  if [ -z "$NGC_ROOT" ]; then
    NGC_ROOT=/etc/nginx
  fi
  if [ -z "$NGC_NGINX_BIN" ]; then
    NGC_NGINX_BIN=nginx
  fi
  if [ -z "$NGC_NGINX_CONF" ]; then
    NGC_NGINX_CONF="$NGC_ROOT/nginx.conf"
    CONF_DEFAULT=1
  fi
}

resolve_base_path() {
  input=$1
  case "$input" in
    /*) candidate=$input ;;
    *) candidate="$NGC_ROOT/$input" ;;
  esac
  case "$candidate" in
    *$'\n'*) die "文件名不能包含换行符" 3 ;;
  esac
  if [ ! -e "$candidate" ] && [ ! -L "$candidate" ]; then
    case "$candidate" in
      *.conf) ;;
      *)
        if [ -e "$candidate.conf" ] || [ -L "$candidate.conf" ]; then
          candidate="$candidate.conf"
        fi
        ;;
    esac
  fi
  printf '%s' "$candidate"
}

validate_root() {
  if [ ! -d "$NGC_ROOT" ]; then
    die "配置根目录不存在: $NGC_ROOT" 3
  fi
  NGC_ROOT_REAL=$(realpath -e -- "$NGC_ROOT") || die "无法解析配置根目录: $NGC_ROOT" 3
}

resolve_path() {
  input=$1
  candidate=$(resolve_base_path "$input")
  resolved=$(realpath -m -- "$candidate") || die "无法解析路径: $candidate" 3
  root_real=$NGC_ROOT_REAL
  case "$resolved" in
    "$root_real"|"$root_real"/*) ;;
    *) die "路径超出配置根目录: $candidate" 3 ;;
  esac
  printf '%s' "$resolved"
}

validate_subdir_inside_root() {
  dir_path=$1
  if [ ! -d "$dir_path" ]; then
    die "目录不存在: $dir_path" 3
  fi
  dir_real=$(realpath -e -- "$dir_path") || die "无法解析目录: $dir_path" 3
  case "$dir_real" in
    "$NGC_ROOT_REAL"|"$NGC_ROOT_REAL"/*) ;;
    *) die "目录超出配置根目录: $dir_path" 3 ;;
  esac
}

is_core() {
  target_path=$1
  case "$target_path" in
    "$NGC_ROOT_REAL"/modules-enabled/*) return 0 ;;
  esac
  base=$(basename -- "$target_path")
  case "$base" in
    nginx.conf|mime.types|fastcgi.conf|fastcgi_params|scgi_params|uwsgi_params|koi-utf|koi-win|win-utf|proxy_params)
      return 0
      ;;
  esac
  return 1
}

classify() {
  target_path=$1
  dir=$(dirname -- "$target_path")
  if is_core "$target_path"; then
    printf 'core'
  elif [ "$dir" = "$NGC_ROOT_REAL/sites-available" ] || [ "$dir" = "$NGC_ROOT_REAL/sites-enabled" ]; then
    printf 'site'
  elif [ "$(basename -- "$dir")" = "conf.d" ]; then
    printf 'confd'
  else
    printf 'other'
  fi
}

state_for() {
  target_path=$1
  type=$2
  link="$NGC_ROOT_REAL/sites-enabled/$(basename -- "$target_path")"
  case "$type" in
    site)
      if [ -L "$link" ]; then
        target=$(realpath -e -- "$link" 2>/dev/null || true)
        if [ -n "$target" ] && [ "$target" = "$target_path" ]; then
          printf 'ENABLED'
        else
          printf '异常'
        fi
      elif [ -e "$link" ]; then
        printf '异常'
      else
        printf 'DISABLED'
      fi
      ;;
    confd)
      case "$(basename -- "$target_path")" in
        *.conf.disabled) printf 'DISABLED' ;;
        *.conf) printf 'ENABLED' ;;
        *) printf '-' ;;
      esac
      ;;
    *)
      printf '-'
      ;;
  esac
}

tree_render() {
  input_file=$1
  render_mode=$2
  LC_ALL=C awk -F "$TAB" -v mode="$render_mode" '
    function add_child(parent, child, kind) {
      if ((parent SUBSEP child) in child_seen) {
        return
      }
      child_seen[parent, child] = 1
      child_count[parent]++
      children[parent, child_count[parent]] = child
      node_type[child] = kind
    }
    function child_cmp(a, b,    ka, kb) {
      ka = (node_type[a] == "dir") ? 0 : 1
      kb = (node_type[b] == "dir") ? 0 : 1
      if (ka != kb) return ka - kb
      if (a < b) return -1
      if (a > b) return 1
      return 0
    }
    function sort_children(parent,    i, j, n, tmp) {
      n = child_count[parent]
      for (i = 2; i <= n; i++) {
        tmp = children[parent, i]
        j = i - 1
        while (j >= 1 && child_cmp(children[parent, j], tmp) > 0) {
          children[parent, j + 1] = children[parent, j]
          j--
        }
        children[parent, j + 1] = tmp
      }
    }
    function indent(depth,    s, i) {
      s = ""
      for (i = 0; i < depth; i++) s = s "  "
      return s
    }
    function name_of(path,    n, parts) {
      n = split(path, parts, "/")
      return parts[n]
    }
    function dir_of(path,    n, parts, out, i) {
      n = split(path, parts, "/")
      if (n <= 1) return ""
      out = parts[1]
      for (i = 2; i < n; i++) out = out "/" parts[i]
      return out
    }
    function disp_width(s, chars, wide) {
      chars = s
      gsub(/[\200-\277]/, "", chars)
      wide = chars
      gsub(/[\001-\177]/, "", wide)
      return length(chars) + length(wide)
    }
    function pad_to(name, width,    s) {
      s = name
      while (disp_width(s) < width) s = s " "
      return s
    }
    function print_tree(parent, depth,    n, i, child, kind, box) {
      sort_children(parent)
      n = child_count[parent]
      for (i = 1; i <= n; i++) {
        child = children[parent, i]
        kind = node_type[child]
        if (kind == "dir") {
          if (mode == "toggle") {
            printf "D\t0\t%s\t%s\t%s/\n", depth, child, indent(depth) name_of(child)
          } else if (mode == "list") {
            printf "%s%s/\n", indent(depth), name_of(child)
          }
          print_tree(child, depth + 1)
        } else {
          leaf_index++
          if (mode == "order") {
            print file_id[child]
          } else if (mode == "list") {
            printf "%s%s%s %s\n", indent(depth), pad_to(file_display[child], name_col - 2 * depth), pad_to(file_state[child], 9), file_type[child]
          } else {
            box = (file_state[child] == "1") ? "[x]" : "[ ]"
            printf "L\t%s\t%s\t%s\t%s\t%s\t%s\n", leaf_index, depth, dir_of(child), indent(depth), box, file_display[child]
          }
        }
      }
    }
    {
      rel = $1
      if (rel == "") next
      n = split(rel, parts, "/")
      parent = ""
      for (i = 1; i < n; i++) {
        child = (parent == "" ? parts[i] : parent "/" parts[i])
        add_child(parent, child, "dir")
        parent = child
      }
      add_child(parent, rel, "file")
      file_display[rel] = $2
      file_state[rel] = $3
      file_type[rel] = $4
      file_id[rel] = $5
      leaf_width = 2 * (n - 1) + disp_width($2)
      if (leaf_width > max_width) max_width = leaf_width
      entry_count++
    }
    END {
      if (mode == "list" && entry_count > 0) {
        name_col = max_width + 2
        if (disp_width("NAME") > name_col) name_col = disp_width("NAME")
        printf "%s%s %s\n", pad_to("NAME", name_col), pad_to("STATUS", 9), "TYPE"
      }
      print_tree("", 0)
    }
  ' "$input_file"
}

nginx_check() {
  "$NGC_NGINX_BIN" -t -c "$NGC_NGINX_CONF"
}

parse_target_flags() {
  TARGET=""
  while [ $# -gt 0 ]; do
    case "$1" in
      --force|--yes)
        FORCE=1
        shift
        ;;
      --)
        shift
        break
        ;;
      -*)
        die "未知选项: $1" 2
        ;;
      *)
        if [ -n "$TARGET" ]; then
          die "只能指定一个目标文件" 2
        fi
        TARGET=$1
        shift
        ;;
    esac
  done
  while [ $# -gt 0 ]; do
    if [ -n "$TARGET" ]; then
      die "只能指定一个目标文件" 2
    fi
    TARGET=$1
    shift
  done
  if [ -z "$TARGET" ]; then
    die "缺少目标文件" 2
  fi
}

print_help() {
  cat <<'EOF'
用法: ngc [全局参数] <子命令> [子命令参数]

子命令:
  list                    列出配置文件及启用状态
  toggle                  交互式批量启用/禁用配置
  edit <file>             交互式修改内置字段
  delete <file> [--force] 永久删除配置文件
  check                   执行 nginx -t
  reload                  校验后重载 Nginx
  update [选项]           自更新
  version                 显示版本
  help                    显示帮助
  __complete <words...>   内部补全接口

全局参数:
  --root DIR              配置根目录（默认 /etc/nginx）
  --nginx PATH            nginx 可执行文件（默认 nginx）
  --conf PATH             校验入口配置（默认 <root>/nginx.conf）
  --force, --yes          覆盖冲突、跳过删除确认、允许核心文件操作
  -h, --help              显示帮助
  -V, --version           显示版本

示例:
  ngc list
  ngc toggle
  ngc edit sites-available/example.com.conf
  ngc delete conf.d/legacy.conf --force
  ngc check
  ngc reload
  ngc update --check
EOF
}

print_update_help() {
  cat <<'EOF'
用法: ngc update [选项]

选项:
  -y, --yes          非交互确认
  --check            仅检查是否有更新；有更新返回 0，无更新返回 1
  --force            允许降级；同版本或降级时强制重装
  --dry-run          只显示计划，不写盘
  --base-url URL     覆盖更新基地址（默认读配置项 update_base_url）
  --insecure         跳过 TLS 校验（NGC_INSECURE=1 等效）
  --prefix DIR       安装前缀（默认由当前脚本路径推导）
  --shell 名称       bash|zsh|auto（缺省自动识别）
  -h, --help         显示本帮助
EOF
}

cmd_help() {
  print_help
}

cmd_version() {
  printf 'ngc %s\n' "$NGC_VERSION"
}

cmd_list() {
  if [ $# -ne 0 ]; then
    die "用法: ngc list" 2
  fi
  require_root
  validate_root
  TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ngc-list.XXXXXX")
  list_file="$TMP_DIR/paths"
  list_entries="$TMP_DIR/list.entries"
  : > "$list_file"
  : > "$list_entries"
  find "$NGC_ROOT_REAL" -type f \( -name '*.conf' -o -name '*.conf.disabled' \) >> "$list_file"
  if [ -d "$NGC_ROOT_REAL/sites-available" ]; then
    find "$NGC_ROOT_REAL/sites-available" -maxdepth 1 -type f >> "$list_file"
  fi
  if [ -d "$NGC_ROOT_REAL/sites-enabled" ]; then
    find "$NGC_ROOT_REAL/sites-enabled" -maxdepth 1 -type l >> "$list_file"
  fi
  LC_ALL=C sort -u "$list_file" -o "$list_file"
  while IFS= read -r target_path; do
    [ -n "$target_path" ] || continue
    if [ -L "$target_path" ] && [ "$(dirname -- "$target_path")" = "$NGC_ROOT_REAL/sites-enabled" ]; then
      target=$(realpath -e -- "$target_path" 2>/dev/null || true)
      if [ -n "$target" ]; then
        case "$target" in
          "$NGC_ROOT_REAL"/*)
            if [ -f "$target" ] \
              && [ "$(basename -- "$target")" = "$(basename -- "$target_path")" ] \
              && grep -Fxq -- "$target" "$list_file"; then
              continue
            fi
            rel=${target_path#"$NGC_ROOT_REAL"/}
            display=$(basename -- "$target_path")
            if [ -f "$target" ]; then
              printf '%s\t%s\t%s\t%s\n' "$rel" "$display" 'ENABLED' 'site' >> "$list_entries"
            else
              printf '%s\t%s\t%s\t%s\n' "$rel" "$display" '异常' 'site' >> "$list_entries"
            fi
            continue
            ;;
        esac
      fi
      rel=${target_path#"$NGC_ROOT_REAL"/}
      display=$(basename -- "$target_path")
      printf '%s\t%s\t%s\t%s\n' "$rel" "$display" '异常' 'site' >> "$list_entries"
      continue
    fi
    type=$(classify "$target_path")
    state=$(state_for "$target_path" "$type")
    rel=${target_path#"$NGC_ROOT_REAL"/}
    display=$(basename -- "$target_path")
    printf '%s\t%s\t%s\t%s\n' "$rel" "$display" "$state" "$type" >> "$list_entries"
  done < "$list_file"
  tree_render "$list_entries" list
}

toggle_parse_flags() {
  while [ $# -gt 0 ]; do
    case "$1" in
      --force|--yes)
        FORCE=1
        shift
        ;;
      *)
        die "未知选项: $1" 2
        ;;
    esac
  done
}

toggle_site_link() {
  printf '%s/sites-enabled/%s' "$NGC_ROOT_REAL" "$(basename -- "$1")"
}

toggle_show_warnings() {
  if [ -s "$TOGGLE_WARNINGS" ]; then
    while IFS= read -r warning; do
      log "$warning"
    done < "$TOGGLE_WARNINGS"
  fi
}

toggle_build_entries() {
  awk -F "$TAB" -v OFS="$TAB" '{
    rel = $2
    n = split(rel, parts, "/")
    print rel, parts[n], $1, $3, NR
  }' "$TOGGLE_ITEMS" > "$TOGGLE_ENTRIES"
}

toggle_order_items() {
  toggle_build_entries
  tree_render "$TOGGLE_ENTRIES" order > "$TOGGLE_ORDER"
  order_count=$(wc -l < "$TOGGLE_ORDER" | tr -d ' ')
  item_count=$(wc -l < "$TOGGLE_ITEMS" | tr -d ' ')
  if [ "$order_count" -ne "$item_count" ]; then
    die "内部错误：树形排序条目数量不一致" 8
  fi
  awk -F "$TAB" 'NR == FNR { order[++n] = $1; next } { line[FNR] = $0 } END { for (i = 1; i <= n; i++) print line[order[i]] }' "$TOGGLE_ORDER" "$TOGGLE_ITEMS" > "$TOGGLE_ITEMS.new"
  mv -f -- "$TOGGLE_ITEMS.new" "$TOGGLE_ITEMS"
  awk -F "$TAB" 'NR == FNR { order[++n] = $1; next } { line[FNR] = $0 } END { for (i = 1; i <= n; i++) print line[order[i]] }' "$TOGGLE_ORDER" "$TOGGLE_ORIG" > "$TOGGLE_ORIG.new"
  mv -f -- "$TOGGLE_ORIG.new" "$TOGGLE_ORIG"
}

toggle_collect() {
  TOGGLE_ITEMS="$TMP_DIR/toggle.items"
  TOGGLE_ORIG="$TMP_DIR/toggle.orig"
  TOGGLE_RAW="$TMP_DIR/toggle.raw"
  TOGGLE_WARNINGS="$TMP_DIR/toggle.warnings"
  TOGGLE_LINKS="$TMP_DIR/toggle.links"
  TOGGLE_ENTRIES="$TMP_DIR/toggle.entries"
  TOGGLE_ORDER="$TMP_DIR/toggle.order"
  TOGGLE_RENDER="$TMP_DIR/toggle.render"
  : > "$TOGGLE_ITEMS"
  : > "$TOGGLE_WARNINGS"
  : > "$TOGGLE_RAW"
  : > "$TOGGLE_LINKS"

  if [ -e "$NGC_ROOT_REAL/sites-available" ] || [ -L "$NGC_ROOT_REAL/sites-available" ]; then
    validate_subdir_inside_root "$NGC_ROOT_REAL/sites-available"
  fi
  if [ -e "$NGC_ROOT_REAL/sites-enabled" ] || [ -L "$NGC_ROOT_REAL/sites-enabled" ]; then
    validate_subdir_inside_root "$NGC_ROOT_REAL/sites-enabled"
  fi

  if [ -d "$NGC_ROOT_REAL/sites-available" ]; then
    find "$NGC_ROOT_REAL/sites-available" -maxdepth 1 -type f -print 2>/dev/null | while IFS= read -r file_path; do
      rel=${file_path#"$NGC_ROOT_REAL"/}
      printf 'site\t%s\t%s\n' "$rel" "$file_path" >> "$TOGGLE_RAW"
    done
  fi

  find "$NGC_ROOT_REAL" -type f \( -name '*.conf' -o -name '*.conf.disabled' \) -print 2>/dev/null | while IFS= read -r file_path; do
    dir=$(dirname -- "$file_path")
    if [ "$(basename -- "$dir")" != "conf.d" ]; then
      continue
    fi
    case "$file_path" in
      *.conf.disabled) base=${file_path%.disabled} ;;
      *) base=$file_path ;;
    esac
    rel=${base#"$NGC_ROOT_REAL"/}
    printf 'confd\t%s\t%s\n' "$rel" "$base" >> "$TOGGLE_RAW"
  done

  LC_ALL=C sort -u -t "$TAB" -k2,2 -k1,1 "$TOGGLE_RAW" > "$TOGGLE_RAW.sorted"

  if [ -d "$NGC_ROOT_REAL/sites-enabled" ]; then
    find "$NGC_ROOT_REAL/sites-enabled" -maxdepth 1 -type l -print 2>/dev/null | while IFS= read -r link; do
      target=$(realpath -e -- "$link" 2>/dev/null || true)
      rel_link=${link#"$NGC_ROOT_REAL"/}
      if [ -z "$target" ]; then
        printf '警告: 断链或目标不存在: %s\n' "$rel_link" >> "$TOGGLE_WARNINGS"
        printf '%s\t\n' "$link" >> "$TOGGLE_LINKS"
        continue
      fi
      case "$target" in
        "$NGC_ROOT_REAL"/*) ;;
        *)
          printf '警告: 软链指向配置根目录外: %s -> %s\n' "$rel_link" "$target" >> "$TOGGLE_WARNINGS"
          printf '%s\t%s\n' "$link" "$target" >> "$TOGGLE_LINKS"
          continue
          ;;
      esac
      if [ "$(dirname -- "$target")" != "$NGC_ROOT_REAL/sites-available" ]; then
        printf '警告: 非标准软链目标: %s -> %s\n' "$rel_link" "$target" >> "$TOGGLE_WARNINGS"
      fi
      printf '%s\t%s\n' "$link" "$target" >> "$TOGGLE_LINKS"
    done
  fi

  while IFS="$TAB" read -r type rel base; do
    [ -n "$type" ] || continue
    case "$type" in
      site)
        [ -f "$base" ] || continue
        link="$NGC_ROOT_REAL/sites-enabled/$(basename -- "$base")"
        links_for="$TMP_DIR/toggle.links-for"
        awk -F "$TAB" -v target="$base" '$2 == target { print $1 }' "$TOGGLE_LINKS" > "$links_for"
        count=$(wc -l < "$links_for" | tr -d ' ')
        if [ "$count" -eq 0 ]; then
          if [ -L "$link" ] || [ -e "$link" ]; then
            printf '警告: sites-enabled 中同名条目异常，已跳过: %s\n' "${link#"$NGC_ROOT_REAL"/}" >> "$TOGGLE_WARNINGS"
            continue
          fi
          state=0
        elif [ "$count" -eq 1 ]; then
          first_link=$(sed -n '1p' "$links_for")
          if [ "$first_link" = "$link" ]; then
            state=1
          else
            printf '警告: 非标准启用的站点已跳过: %s -> %s\n' "${first_link#"$NGC_ROOT_REAL"/}" "$base" >> "$TOGGLE_WARNINGS"
            continue
          fi
        else
          printf '警告: 站点存在多个启用软链，已跳过: %s\n' "$base" >> "$TOGGLE_WARNINGS"
          continue
        fi
        printf '%s\t%s\t%s\t%s\n' "$state" "$rel" "$type" "$base" >> "$TOGGLE_ITEMS"
        ;;
      confd)
        has_enabled=0
        has_disabled=0
        if [ -f "$base" ]; then
          has_enabled=1
        fi
        if [ -f "$base.disabled" ]; then
          has_disabled=1
        fi
        if [ "$has_enabled" -eq 1 ] && [ "$has_disabled" -eq 1 ]; then
          printf '警告: conf.d 同名配置同时存在 .conf 与 .conf.disabled，已跳过: %s\n' "$rel" >> "$TOGGLE_WARNINGS"
          continue
        fi
        if [ "$has_enabled" -eq 1 ]; then
          state=1
        elif [ "$has_disabled" -eq 1 ]; then
          state=0
        else
          continue
        fi
        printf '%s\t%s\t%s\t%s\n' "$state" "$rel" "$type" "$base" >> "$TOGGLE_ITEMS"
        ;;
    esac
  done < "$TOGGLE_RAW.sorted"

  cp "$TOGGLE_ITEMS" "$TOGGLE_ORIG"
  toggle_order_items
}

toggle_read_byte() {
  dd bs=1 count=1 2>/dev/null | tr -d '\000' || true
}

toggle_rows() {
  size=$(stty size 2>/dev/null || true)
  if [ -n "$size" ]; then
    rows=${size%% *}
    case "$rows" in
      ''|*[!0-9]*) rows=24 ;;
    esac
  else
    rows=24
  fi
  printf '%s' "$rows"
}

toggle_render() {
  rows=$(toggle_rows)
  win=$((rows - 6))
  if [ "$win" -lt 3 ]; then
    win=3
  fi

  toggle_build_entries
  tree_render "$TOGGLE_ENTRIES" toggle > "$TOGGLE_RENDER"

  max_depth=$(awk -F "$TAB" '{ if ($3 + 0 > m) m = $3 + 0 } END { print m + 0 }' "$TOGGLE_RENDER")
  win=$((win - max_depth))
  if [ "$win" -lt 3 ]; then
    win=3
  fi

  total_items=$(wc -l < "$TOGGLE_ITEMS" | tr -d ' ')
  total_lines=$(wc -l < "$TOGGLE_RENDER" | tr -d ' ')
  cursor_line=$(awk -F "$TAB" -v c=$((TOGGLE_CURSOR + 1)) '$1 == "L" && $2 == c { print NR; exit }' "$TOGGLE_RENDER")
  if [ -z "$cursor_line" ]; then
    die "内部错误：无法定位当前条目" 8
  fi

  start_line=$((TOGGLE_OFFSET + 1))
  if [ "$start_line" -lt 1 ]; then
    start_line=1
  fi
  if [ "$cursor_line" -lt "$start_line" ]; then
    start_line=$cursor_line
  elif [ "$cursor_line" -gt $((start_line + win - 1)) ]; then
    start_line=$((cursor_line - win + 1))
  fi
  if [ "$start_line" -lt 1 ]; then
    start_line=1
  fi
  end_line=$((start_line + win - 1))
  if [ "$end_line" -gt "$total_lines" ]; then
    end_line=$total_lines
  fi
  TOGGLE_OFFSET=$((start_line - 1))

  first_leaf_dir=$(awk -F "$TAB" -v s="$start_line" -v e="$end_line" 'NR >= s && NR <= e && $1 == "L" { print $4; exit }' "$TOGGLE_RENDER")
  missing_headers=""
  if [ -n "$first_leaf_dir" ]; then
    missing_headers=$(awk -F "$TAB" -v s="$start_line" -v dir="$first_leaf_dir" '
      BEGIN {
        n = split(dir, parts, "/")
        p = ""
        for (i = 1; i <= n; i++) {
          p = (i == 1 ? parts[i] : p "/" parts[i])
          ancestor[p] = 1
        }
      }
      NR < s && $1 == "D" && ($4 in ancestor) { print $5 }
    ' "$TOGGLE_RENDER")
  fi

  selected=$(awk -F "$TAB" '$1 == "1" { c++ } END { print c + 0 }' "$TOGGLE_ITEMS")
  pending=$(awk -F "$TAB" 'NR == FNR { o[NR] = $1; next } $1 != o[FNR] { c++ } END { print c + 0 }' "$TOGGLE_ORIG" "$TOGGLE_ITEMS")
  warnings=0
  if [ -s "$TOGGLE_WARNINGS" ]; then
    warnings=$(wc -l < "$TOGGLE_WARNINGS" | tr -d ' ')
  fi

  printf '\033[H\033[2J'
  printf '使用 ↑/↓ 或 j/k 移动，空格切换，Enter 应用，Esc/q 取消\n\n'
  if [ -n "$missing_headers" ]; then
    printf '%s\n' "$missing_headers"
  fi
  awk -F "$TAB" -v s="$start_line" -v e="$end_line" -v c=$((TOGGLE_CURSOR + 1)) '
    NR < s || NR > e { next }
    $1 == "D" { print $5; next }
    $1 == "L" {
      mark = ($2 == c) ? ">" : " "
      printf "%s%s %s %s\n", $5, mark, $6, $7
    }
  ' "$TOGGLE_RENDER"

  first_item=$(awk -F "$TAB" -v s="$start_line" -v e="$end_line" 'NR >= s && NR <= e && $1 == "L" { print $2; exit }' "$TOGGLE_RENDER")
  last_item=$(awk -F "$TAB" -v s="$start_line" -v e="$end_line" 'NR >= s && NR <= e && $1 == "L" { last = $2 } END { print last + 0 }' "$TOGGLE_RENDER")
  if [ "$start_line" -gt 1 ] || [ "$end_line" -lt "$total_lines" ]; then
    printf '\n条目 %s-%s/%s，已启用 %s 项，待变更 %s 项' "$first_item" "$last_item" "$total_items" "$selected" "$pending"
  else
    printf '\n已启用 %s 项，待变更 %s 项' "$selected" "$pending"
  fi
  if [ "$warnings" -gt 0 ]; then
    printf '，异常条目 %s 项（退出后显示）' "$warnings"
  fi
  printf '\n'
}

toggle_flip() {
  line=$1
  tmp="$TMP_DIR/toggle.items.new"
  awk -F "$TAB" -v OFS="$TAB" -v n="$line" 'NR == n { $1 = ($1 == "1" ? "0" : "1") } { print }' "$TOGGLE_ITEMS" > "$tmp"
  mv -f -- "$tmp" "$TOGGLE_ITEMS"
}

toggle_move_up() {
  if [ "$TOGGLE_CURSOR" -gt 0 ]; then
    TOGGLE_CURSOR=$((TOGGLE_CURSOR - 1))
  fi
}

toggle_move_down() {
  total=$(wc -l < "$TOGGLE_ITEMS" | tr -d ' ')
  if [ "$TOGGLE_CURSOR" -lt $((total - 1)) ]; then
    TOGGLE_CURSOR=$((TOGGLE_CURSOR + 1))
  fi
}

toggle_term_enter() {
  TTY_SAVED=$(stty -g) || die "无法读取终端状态" 8
  TUI_ACTIVE=1
  printf '\033[?25l'
  stty -echo -icanon min 1 time 0 || die "无法设置终端模式" 8
}

toggle_term_leave() {
  if [ -n "${TTY_SAVED:-}" ]; then
    stty "$TTY_SAVED" 2>/dev/null || true
    TTY_SAVED=""
  fi
  if [ "${TUI_ACTIVE:-0}" -eq 1 ]; then
    printf '\033[?25h'
    TUI_ACTIVE=0
  fi
}

toggle_interact() {
  TOGGLE_CURSOR=0
  TOGGLE_OFFSET=0
  TOGGLE_APPLY=1
  toggle_term_enter
  while :; do
    toggle_render
    key=$(toggle_read_byte)
    case "$key" in
      ' ')
        toggle_flip "$((TOGGLE_CURSOR + 1))"
        ;;
      j)
        toggle_move_down
        ;;
      k)
        toggle_move_up
        ;;
      q)
        TOGGLE_APPLY=0
        break
        ;;
      $'\033')
        stty -echo -icanon min 0 time 1
        seq2=$(toggle_read_byte)
        if [ "$seq2" = "[" ] || [ "$seq2" = "O" ]; then
          stty -echo -icanon min 1 time 0
          seq3=$(toggle_read_byte)
          case "$seq3" in
            A) toggle_move_up ;;
            B) toggle_move_down ;;
          esac
        else
          stty -echo -icanon min 1 time 0
          TOGGLE_APPLY=0
          break
        fi
        ;;
      $'\r'|'')
        break
        ;;
      $'\003')
        TOGGLE_APPLY=0
        break
        ;;
    esac
  done
  toggle_term_leave
  printf '\033[H\033[2J'
}

toggle_record() {
  kind=$1
  shift
  printf '%s' "$kind" >> "$TOGGLE_ROLLBACK"
  for field in "$@"; do
    printf '\t%s' "$field" >> "$TOGGLE_ROLLBACK"
  done
  printf '\n' >> "$TOGGLE_ROLLBACK"
}

toggle_backup() {
  backup=$(mktemp "${TMPDIR:-/tmp}/ngc-toggle.XXXXXX") || die "无法创建备份文件" 8
  cp -p -- "$1" "$backup" || die "备份失败: $1" 8
  printf '%s' "$backup"
}

toggle_remove_backups() {
  if [ ! -s "$TOGGLE_ROLLBACK" ]; then
    return 0
  fi
  while IFS="$TAB" read -r kind f1 f2; do
    case "$kind" in
      FILE_REMOVED|BACKUP_OVERWRITTEN)
        rm -f -- "$f2"
        ;;
    esac
  done < "$TOGGLE_ROLLBACK"
}

toggle_rollback() {
  if [ ! -s "$TOGGLE_ROLLBACK" ]; then
    return 0
  fi
  reverse="$TMP_DIR/toggle.rollback.rev"
  awk '{ a[NR] = $0 } END { for (i = NR; i >= 1; i--) print a[i] }' "$TOGGLE_ROLLBACK" > "$reverse"
  failed=0
  while IFS="$TAB" read -r kind f1 f2; do
    case "$kind" in
      LINK_CREATED)
        rm -f -- "$f1" || failed=1
        ;;
      LINK_REMOVED)
        ln -s -- "$f2" "$f1" || failed=1
        ;;
      FILE_REMOVED)
        cp -p -- "$f2" "$f1" || failed=1
        ;;
      RENAMED)
        mv -f -- "$f2" "$f1" || failed=1
        ;;
      BACKUP_OVERWRITTEN)
        rm -f -- "$f1"
        cp -p -- "$f2" "$f1" || failed=1
        ;;
      *)
        failed=1
        ;;
    esac
  done < "$reverse"
  return "$failed"
}

toggle_fail() {
  message=$1
  if toggle_rollback; then
    toggle_remove_backups
    die "$message；已回滚本次变更" 8
  fi
  die "$message；回滚失败，请人工处理" 8
}

toggle_preflight() {
  while IFS="$TAB" read -r action type base rel; do
    case "$type:$action" in
      site:enable)
        if [ ! -f "$base" ]; then
          die "源文件不存在: $rel" 4
        fi
        link=$(toggle_site_link "$base")
        if [ -L "$link" ]; then
          current=$(realpath -e -- "$link" 2>/dev/null || true)
          if [ "$current" = "$base" ]; then
            continue
          fi
        fi
        if [ -e "$link" ] || [ -L "$link" ]; then
          if [ "$FORCE" -ne 1 ]; then
            die "目标已存在冲突，使用 --force: ${link#"$NGC_ROOT_REAL"/}" 5
          fi
          if [ ! -L "$link" ] && [ ! -f "$link" ]; then
            die "目标位置冲突且类型不支持: $link" 5
          fi
        fi
        ;;
      site:disable)
        if [ ! -f "$base" ]; then
          die "源文件不存在: $rel" 4
        fi
        link=$(toggle_site_link "$base")
        if [ ! -e "$link" ] && [ ! -L "$link" ]; then
          continue
        fi
        if [ -L "$link" ]; then
          current=$(realpath -e -- "$link" 2>/dev/null || true)
          if [ -n "$current" ] && [ "$current" = "$base" ]; then
            continue
          fi
        fi
        if [ "$FORCE" -ne 1 ]; then
          die "目标状态冲突，使用 --force: ${link#"$NGC_ROOT_REAL"/}" 5
        fi
        if [ ! -L "$link" ] && [ ! -f "$link" ]; then
          die "目标不是普通文件或软链: $link" 5
        fi
        ;;
      confd:enable)
        if [ -e "$base" ] && [ ! -f "$base" ]; then
          die "目标位置冲突且类型不支持: $base" 5
        fi
        if [ -f "$base" ]; then
          if [ -f "$base.disabled" ]; then
            die "conf.d 同名配置同时存在 .conf 与 .conf.disabled: $rel" 5
          fi
          continue
        fi
        if [ ! -f "$base.disabled" ]; then
          die "源文件不存在: $rel" 4
        fi
        ;;
      confd:disable)
        if [ -e "$base.disabled" ] && [ ! -f "$base.disabled" ]; then
          die "目标位置冲突且类型不支持: $base.disabled" 5
        fi
        if [ -f "$base.disabled" ]; then
          if [ -f "$base" ]; then
            die "conf.d 同名配置同时存在 .conf 与 .conf.disabled: $rel" 5
          fi
          continue
        fi
        if [ ! -f "$base" ]; then
          die "源文件不存在: $rel" 4
        fi
        ;;
      *)
        die "内部错误：未知操作 $type:$action" 8
        ;;
    esac
  done < "$TOGGLE_PLAN"
}

toggle_apply_site_enable() {
  source=$1
  link=$2
  if [ -L "$link" ]; then
    current=$(realpath -e -- "$link" 2>/dev/null || true)
    if [ "$current" = "$source" ]; then
      return 0
    fi
    old_target=$(readlink -- "$link" || true)
    rm -f -- "$link" || toggle_fail "删除冲突软链失败: $link"
    if [ -n "$old_target" ]; then
      toggle_record LINK_REMOVED "$link" "$old_target"
    fi
  elif [ -e "$link" ]; then
    backup=$(toggle_backup "$link")
    rm -f -- "$link" || toggle_fail "删除冲突文件失败: $link"
    toggle_record FILE_REMOVED "$link" "$backup"
  fi
  ln -s -- "$source" "$link" || toggle_fail "创建软链失败: $link"
  toggle_record LINK_CREATED "$link"
}

toggle_apply_site_disable() {
  source=$1
  link=$2
  if [ ! -e "$link" ] && [ ! -L "$link" ]; then
    return 0
  fi
  if [ -L "$link" ]; then
    old_target=$(readlink -- "$link" || true)
    rm -f -- "$link" || toggle_fail "删除软链失败: $link"
    if [ -n "$old_target" ]; then
      toggle_record LINK_REMOVED "$link" "$old_target"
    fi
  elif [ -f "$link" ]; then
    backup=$(toggle_backup "$link")
    rm -f -- "$link" || toggle_fail "删除文件失败: $link"
    toggle_record FILE_REMOVED "$link" "$backup"
  else
    die "目标不是普通文件或软链: $link" 5
  fi
}

toggle_apply_confd_enable() {
  base=$1
  source="$base.disabled"
  dest=$base
  if [ -f "$dest" ]; then
    if [ -f "$source" ]; then
      die "conf.d 同名配置同时存在 .conf 与 .conf.disabled: $dest" 5
    fi
    return 0
  fi
  if [ ! -f "$source" ]; then
    toggle_fail "源文件不存在: $source"
  fi
  if [ -e "$dest" ]; then
    backup=$(toggle_backup "$dest")
    toggle_record BACKUP_OVERWRITTEN "$dest" "$backup"
  fi
  mv -f -- "$source" "$dest" || toggle_fail "重命名失败: $source"
  toggle_record RENAMED "$source" "$dest"
}

toggle_apply_confd_disable() {
  base=$1
  source=$base
  dest="$base.disabled"
  if [ -f "$dest" ]; then
    if [ -f "$source" ]; then
      die "conf.d 同名配置同时存在 .conf 与 .conf.disabled: $dest" 5
    fi
    return 0
  fi
  if [ ! -f "$source" ]; then
    toggle_fail "源文件不存在: $source"
  fi
  if [ -e "$dest" ]; then
    backup=$(toggle_backup "$dest")
    toggle_record BACKUP_OVERWRITTEN "$dest" "$backup"
  fi
  mv -f -- "$source" "$dest" || toggle_fail "重命名失败: $source"
  toggle_record RENAMED "$source" "$dest"
}

toggle_apply() {
  TOGGLE_PLAN="$TMP_DIR/toggle.plan"
  TOGGLE_ROLLBACK="$TMP_DIR/toggle.rollback"
  : > "$TOGGLE_ROLLBACK"
  awk -F "$TAB" -v OFS="$TAB" 'NR == FNR { o[NR] = $1; next } $1 != o[FNR] { print ($1 == "1" ? "enable" : "disable"), $3, $4, $2 }' "$TOGGLE_ORIG" "$TOGGLE_ITEMS" > "$TOGGLE_PLAN"

  toggle_preflight

  while IFS="$TAB" read -r action type base rel; do
    case "$type:$action" in
      site:enable)
        toggle_apply_site_enable "$base" "$(toggle_site_link "$base")"
        ;;
      site:disable)
        toggle_apply_site_disable "$base" "$(toggle_site_link "$base")"
        ;;
      confd:enable)
        toggle_apply_confd_enable "$base"
        ;;
      confd:disable)
        toggle_apply_confd_disable "$base"
        ;;
    esac
  done < "$TOGGLE_PLAN"

  if ! nginx_check; then
    if toggle_rollback; then
      toggle_remove_backups
      die "校验失败，已回滚本次全部变更" 6
    fi
    die "校验失败且回滚失败，请人工处理" 8
  fi
  toggle_remove_backups

  while IFS="$TAB" read -r action type base rel; do
    case "$action" in
      enable) info "已启用: $rel" ;;
      disable) info "已禁用: $rel" ;;
    esac
  done < "$TOGGLE_PLAN"
  info "校验成功。"
}

cmd_toggle() {
  toggle_parse_flags "$@"
  require_root
  validate_root
  TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ngc-toggle.XXXXXX")
  toggle_collect

  total=$(wc -l < "$TOGGLE_ITEMS" | tr -d ' ')
  if [ "$total" -eq 0 ]; then
    info "没有支持启用/禁用的配置。"
    toggle_show_warnings
    return 0
  fi
  if [ ! -t 0 ]; then
    die "ngc toggle 需要在交互式终端中运行" 2
  fi

  toggle_interact

  if [ "$TOGGLE_APPLY" -ne 1 ]; then
    toggle_show_warnings
    info "已取消，未做修改。"
    return 0
  fi

  pending=$(awk -F "$TAB" 'NR == FNR { o[NR] = $1; next } $1 != o[FNR] { c++ } END { print c + 0 }' "$TOGGLE_ORIG" "$TOGGLE_ITEMS")
  toggle_show_warnings
  if [ "$pending" -eq 0 ]; then
    info "无变更。"
    return 0
  fi

  acquire_lock
  toggle_apply
}

context_for_line() {
  awk -v target="$2" '
    BEGIN { depth = 0 }
    {
      if (NR == target) {
        for (i = depth; i >= 1; i--) {
          if (stack[i] != "") {
            print stack[i]
            exit
          }
        }
        print "-"
        exit
      }
      line = $0
      sub(/#.*/, "", line)
      opens = gsub(/\{/, "{", line)
      closes = gsub(/\}/, "}", line)
      for (i = 0; i < opens; i++) {
        depth++
        hdr = $0
        sub(/\{.*$/, "", hdr)
        sub(/^[[:space:]]+/, "", hdr)
        sub(/[[:space:]]+$/, "", hdr)
        if (hdr ~ /^(server|location|upstream|http|stream|rtmp)([[:space:]]|$)/) {
          stack[depth] = hdr
        } else {
          stack[depth] = ""
        }
      }
      for (i = 0; i < closes; i++) {
        stack[depth] = ""
        if (depth > 0) {
          depth--
        }
      }
    }
  ' "$1"
}

directive_occurrences() {
  content=$1
  directive=$2
  printf '%s\n' "$content" | awk -v d="$directive" '
    {
      line = $0
      sub(/#.*/, "", line)
      n = 0
      s = line
      pattern = "(^|[{};])[[:space:]]*" d "([[:space:]]|;|$)"
      while (match(s, pattern)) {
        n++
        s = substr(s, RSTART + RLENGTH)
      }
      print n
    }
  '
}

replace_directive_value() {
  src=$1
  dst=$2
  lineno=$3
  value=$4
  awk -v target="$lineno" -v val="$value" '
    NR == target {
      line = $0
      if (index(line, ";") == 0) {
        print line
        next
      }
      lead = ""
      rest = line
      if (match(rest, /^[[:space:]]+/)) {
        lead = substr(rest, 1, RLENGTH)
        rest = substr(rest, RLENGTH + 1)
      }
      if (match(rest, /^[^[:space:]]+[[:space:]]+/)) {
        pref = substr(rest, 1, RLENGTH)
        after = substr(rest, RLENGTH + 1)
      } else {
        print line
        next
      }
      pos = index(after, ";")
      if (pos == 0) {
        print line
        next
      }
      suffix = substr(after, pos)
      print lead pref val suffix
      next
    }
    { print }
  ' "$src" > "$dst"
}

trim_text() {
  printf '%s' "$1" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
}

do_replace() {
  target_path=$1
  directive=$2
  matches=$(mktemp "${TMPDIR:-/tmp}/ngc-matches.XXXXXX")
  nums=$(mktemp "${TMPDIR:-/tmp}/ngc-nums.XXXXXX")
  if ! grep -nE "^[[:space:]]*${directive}[[:space:]]" "$target_path" 2>/dev/null | grep -F ';' > "$matches"; then
    :
  fi
  count=$(wc -l < "$matches" | tr -d ' ')
  if [ "$count" -eq 0 ]; then
    log "未找到指令: $directive"
    rm -f -- "$matches" "$nums"
    return 1
  fi
  while IFS= read -r entry; do
    printf '%s\n' "${entry%%:*}" >> "$nums"
  done < "$matches"
  chosen=""
  if [ "$count" -eq 1 ]; then
    chosen=$(sed -n '1p' "$nums")
  else
    printf '发现 %s 处匹配:\n' "$count"
    idx=0
    while IFS= read -r entry; do
      idx=$((idx + 1))
      lineno=${entry%%:*}
      content=${entry#*:}
      trimmed=$(trim_text "$content")
      ctx=$(context_for_line "$target_path" "$lineno")
      printf '  %d) 行 %s: %s [%s]\n' "$idx" "$lineno" "$trimmed" "$ctx"
    done < "$matches"
    printf '选择要修改的匹配 [1-%s] (0 取消): ' "$count"
    if ! read -r selection; then
      rm -f -- "$matches" "$nums"
      return 1
    fi
    case "$selection" in
      ''|0) rm -f -- "$matches" "$nums"; return 1 ;;
      *[!0-9]*) log "无效选择"; rm -f -- "$matches" "$nums"; return 1 ;;
    esac
    if [ "$selection" -lt 1 ] || [ "$selection" -gt "$count" ]; then
      log "无效选择"
      rm -f -- "$matches" "$nums"
      return 1
    fi
    chosen=$(sed -n "${selection}p" "$nums")
  fi
  content=$(sed -n "${chosen}p" "$target_path")
  occ=$(directive_occurrences "$content" "$directive")
  if [ "$occ" -gt 1 ]; then
    log "同一行出现多条同名指令，无法安全替换，请手动调整"
    rm -f -- "$matches" "$nums"
    return 1
  fi
  printf '输入新的 %s 值 (空值取消): ' "$directive"
  if ! read -r value; then
    rm -f -- "$matches" "$nums"
    return 1
  fi
  value=$(trim_text "$value")
  if [ -z "$value" ]; then
    rm -f -- "$matches" "$nums"
    return 1
  fi
  case "$value" in
    *';'*|*'{'*|*'}'*|*'#'*)
      log "新值不能包含 ; { } #"
      rm -f -- "$matches" "$nums"
      return 1
      ;;
  esac
  case "$value" in
    *$'\r'*|*$'\n'*)
      log "新值不能包含换行符"
      rm -f -- "$matches" "$nums"
      return 1
      ;;
  esac
  dir=$(dirname -- "$target_path")
  tmp=$(mktemp "$dir/.ngc.XXXXXX") || die "无法创建临时文件" 8
  EDIT_TMP=$tmp
  if ! cp -p -- "$target_path" "$tmp"; then
    rm -f -- "$tmp"
    EDIT_TMP=""
    die "备份文件失败" 8
  fi
  if ! replace_directive_value "$target_path" "$tmp" "$chosen" "$value"; then
    rm -f -- "$tmp"
    EDIT_TMP=""
    die "写入修改失败" 8
  fi
  if ! mv -f -- "$tmp" "$target_path"; then
    rm -f -- "$tmp"
    EDIT_TMP=""
    die "原子替换失败" 8
  fi
  EDIT_TMP=""
  rm -f -- "$matches" "$nums"
  info "已修改 $directive (行 $chosen)"
  return 0
}

print_edit_menu() {
  cat <<'EOF'
可编辑字段:
  1) server_name
  2) listen
  3) root
  4) proxy_pass
  5) ssl_certificate
  6) ssl_certificate_key
  7) client_max_body_size
  8) index
  9) access_log
 10) error_log
  0) 取消操作
EOF
}

cmd_edit() {
  parse_target_flags "$@"
  require_root
  validate_root
  acquire_lock
  target_path=$(resolve_path "$TARGET")
  if [ ! -f "$target_path" ]; then
    die "目标文件不存在或不是普通文件: $TARGET" 4
  fi
  backup=$(mktemp "${TMPDIR:-/tmp}/ngc-edit.XXXXXX") || die "无法创建备份" 8
  cp -p -- "$target_path" "$backup" || die "无法备份文件" 8
  EDIT_CHANGED=0
  EDIT_VALID=1
  while :; do
    print_edit_menu
    printf '请选择字段 (0 取消): '
    if ! read -r choice; then
      break
    fi
    case "$choice" in
      0|'') break ;;
      1) directive=server_name ;;
      2) directive=listen ;;
      3) directive=root ;;
      4) directive=proxy_pass ;;
      5) directive=ssl_certificate ;;
      6) directive=ssl_certificate_key ;;
      7) directive=client_max_body_size ;;
      8) directive=index ;;
      9) directive=access_log ;;
      10) directive=error_log ;;
      *) log "无效选项"; continue ;;
    esac
    if do_replace "$target_path" "$directive"; then
      EDIT_CHANGED=$((EDIT_CHANGED + 1))
      if nginx_check; then
        info "校验通过"
      else
        EDIT_VALID=0
        info "校验失败，修改已保留"
      fi
    fi
  done
  if nginx_check; then
    final_ok=1
  else
    final_ok=0
    EDIT_VALID=0
  fi
  if [ "$final_ok" -eq 1 ] && [ "$EDIT_VALID" -eq 1 ]; then
    rm -f -- "$backup"
    printf '修改数量: %s；校验结果: 通过\n' "$EDIT_CHANGED"
    return 0
  fi
  printf '修改数量: %s；校验结果: 失败\n' "$EDIT_CHANGED"
  printf '备份保留：%s\n' "$backup"
  return 6
}

cmd_delete() {
  parse_target_flags "$@"
  require_root
  validate_root
  acquire_lock
  raw=$(resolve_base_path "$TARGET")
  if [ -L "$raw" ]; then
    die "请对软链的源文件操作" 4
  fi
  target_path=$(resolve_path "$TARGET")
  if [ -d "$target_path" ]; then
    die "不允许删除目录: $TARGET" 4
  fi
  if [ ! -f "$target_path" ]; then
    die "目标文件不存在或不是普通文件: $TARGET" 4
  fi
  if is_core "$target_path" && [ "$FORCE" -ne 1 ]; then
    die "核心配置删除需 --force" 5
  fi
  if [ "$FORCE" -ne 1 ]; then
    printf '永久删除 %s？[y/N] ' "$target_path" >&2
    if ! read -r answer; then
      answer=""
    fi
    case "$answer" in
      y|Y) ;;
      *) info "已取消"; return 0 ;;
    esac
  fi
  if ! rm -f -- "$target_path"; then
    die "删除失败: $target_path" 8
  fi
  if ! nginx_check; then
    log "警告: 文件已永久删除且校验失败，请人工修复"
    return 6
  fi
  info "已删除: $target_path"
}

cmd_check() {
  if [ $# -ne 0 ]; then
    die "用法: ngc check" 2
  fi
  require_root
  validate_root
  if nginx_check; then
    info "配置校验通过"
    return 0
  fi
  die "配置校验失败" 6
}

cmd_reload() {
  if [ $# -ne 0 ]; then
    die "用法: ngc reload" 2
  fi
  require_root
  validate_root
  if ! nginx_check; then
    die "配置校验失败，拒绝重载" 6
  fi
  if ! "$NGC_NGINX_BIN" -s reload; then
    die "重载失败" 7
  fi
  info "已重载 Nginx"
}

download_file() {
  url=$1
  dest=$2
  if command -v curl >/dev/null 2>&1; then
    if [ "$DL_INSECURE" -eq 1 ]; then
      curl -fLk --connect-timeout 15 -o "$dest" "$url"
    else
      curl -fsSL --connect-timeout 15 -o "$dest" "$url"
    fi
  elif command -v wget >/dev/null 2>&1; then
    if [ "$DL_INSECURE" -eq 1 ]; then
      wget --no-check-certificate -q -O "$dest" "$url"
    else
      wget -q -O "$dest" "$url"
    fi
  else
    die "需要 curl 或 wget 下载" 1
  fi
}

sha256_of() {
  file=$1
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum -- "$file" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 -- "$file" | awk '{print $1}'
  else
    die "需要 sha256sum 或 shasum" 1
  fi
}

version_gt() {
  left=$1
  right=$2
  if [ "$left" = "$right" ]; then
    return 1
  fi
  newest=$(printf '%s\n%s\n' "$left" "$right" | LC_ALL=C sort -V | tail -n 1)
  [ "$newest" = "$left" ]
}

resolve_update_base() {
  base=""
  if [ -n "${UPDATE_BASE_ARG:-}" ]; then
    base=$UPDATE_BASE_ARG
  else
    base=$(sanitize_config_value "$(config_get update_base_url)")
  fi
  base=${base%/}
  if [ -z "$base" ]; then
    die "未配置更新基地址：请使用 --base-url 或在 $NGC_CONFIG_FILE 中设置 update_base_url" 2
  fi
  printf '%s' "$base"
}

warn_legacy_config() {
  legacy_config="${XDG_CONFIG_HOME:-${HOME:-$PWD}/.config}/ngc/config"
  if [ ! -f "$NGC_CONFIG_FILE" ] && [ -f "$legacy_config" ]; then
    log "提示: 检测到旧配置 $legacy_config，新版本不再读取。"
    log "      请手动迁移到 $NGC_CONFIG_FILE，并补充 update_base_url=..."
  fi
}

detect_platform() {
  os_name=$(uname -s 2>/dev/null || printf 'Linux')
  case "$os_name" in
    Darwin) printf 'macos' ;;
    *) printf 'unix' ;;
  esac
}

manifest_field() {
  manifest_file=$1
  manifest_key=$2
  if [ ! -f "$manifest_file" ]; then
    return 0
  fi
  sed -nE "s/^[[:space:]]*\"${manifest_key}\"[[:space:]]*:[[:space:]]*\"(.*)\"[[:space:]]*,?[[:space:]]*$/\\1/p" "$manifest_file" | head -n 1
}

manifest_files() {
  manifest_file=$1
  if [ ! -f "$manifest_file" ]; then
    return 0
  fi
  sed -n '/"files"[[:space:]]*:/,/\]/p' "$manifest_file" | sed -e '1d' -e '/\][[:space:]]*$/d' -e 's/^[[:space:]]*"//' -e 's/",\{0,1\}[[:space:]]*$//' -e '/^[[:space:]]*$/d'
}

manifest_notes() {
  manifest_file=$1
  raw=$(manifest_field "$manifest_file" notes)
  printf '%s' "$raw" | sed -e 's/\\"/"/g' -e 's/\\\\/\\/g'
}

validate_manifest_files() {
  manifest_file=$1
  manifest_files "$manifest_file" | while IFS= read -r rel; do
    [ -n "$rel" ] || continue
    case "$rel" in
      /*) die "更新清单 files 不可为绝对路径: $rel" 1 ;;
      ..|../*|*/..|*/../*) die "更新清单 files 含越界路径: $rel" 1 ;;
    esac
  done
}

local_version() {
  version_prefix=${1:-$NGC_PREFIX}
  v=""
  if [ -f "$version_prefix/share/$NGC_PROJECT/install-record" ]; then
    v=$(sed -n 's/^# version: //p' "$version_prefix/share/$NGC_PROJECT/install-record" | head -n 1)
  fi
  if [ -z "$v" ] && [ -f "$version_prefix/version" ]; then
    v=$(tr -d '[:space:]' < "$version_prefix/version")
  fi
  if [ -z "$v" ]; then
    v=$NGC_VERSION
  fi
  case "$v" in
    ''|@*@) die "无法确定本地版本" 2 ;;
  esac
  v=${v#v}
  printf '%s' "$v"
}


cmd_update() {
  UPDATE_BASE_ARG=""
  update_yes=$YES_FLAG
  update_check=0
  update_dry=0
  update_prefix=""
  update_shell=""
  UPDATE_FORCE=$FORCE
  DL_INSECURE=0
  while [ $# -gt 0 ]; do
    case "$1" in
      -y|--yes) update_yes=1; shift ;;
      --check) update_check=1; shift ;;
      --force) UPDATE_FORCE=1; shift ;;
      --dry-run) update_dry=1; shift ;;
      --base-url)
        [ $# -ge 2 ] || die "--base-url 需要参数" 2
        UPDATE_BASE_ARG=$2
        shift 2
        ;;
      --base-url=*) UPDATE_BASE_ARG=${1#*=}; shift ;;
      --insecure) DL_INSECURE=1; shift ;;
      --prefix)
        [ $# -ge 2 ] || die "--prefix 需要参数" 2
        update_prefix=$2
        shift 2
        ;;
      --prefix=*) update_prefix=${1#*=}; shift ;;
      --shell)
        [ $# -ge 2 ] || die "--shell 需要参数" 2
        update_shell=$2
        shift 2
        ;;
      --shell=*) update_shell=${1#*=}; shift ;;
      -h|--help) print_update_help; return 0 ;;
      --) shift; break ;;
      -*) die "未知选项: $1" 2 ;;
      *) die "update 不接受位置参数: $1" 2 ;;
    esac
  done
  if [ $# -gt 0 ]; then
    die "update 不接受位置参数: $1" 2
  fi
  case "$update_shell" in
    ''|bash|zsh|auto) ;;
    *) die "--shell 只能是 bash|zsh|auto" 2 ;;
  esac
  if [ "${NGC_INSECURE:-0}" = "1" ] || [ "${NGINX_CONFIG_CONTROL_INSECURE:-0}" = "1" ]; then
    DL_INSECURE=1
  fi
  if [ "$update_check" -eq 0 ] && [ "$update_dry" -eq 0 ] && [ "$update_yes" -ne 1 ] && [ ! -t 0 ]; then
    die "非交互模式更新需要 -y/--yes" 2
  fi
  if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
    die "需要 curl 或 wget" 1
  fi
  warn_legacy_config
  base=$(resolve_update_base)
  export NGC_UPDATE_BASE_URL="$base"
  platform=$(detect_platform)
  if [ "$platform" != "unix" ]; then
    die "当前平台 $platform 未提供更新制品（本项目仅发布 unix）" 2
  fi
  if [ -z "$update_prefix" ]; then
    update_prefix=$NGC_PREFIX
  fi
  update_prefix=${update_prefix%/}
  case "$update_prefix" in
    /*) ;;
    *) die "--prefix 必须是绝对路径: $update_prefix" 2 ;;
  esac
  dl_root="$base/$NGC_PROJECT/$platform"
  TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ngc-update.XXXXXX") || die "无法创建临时目录" 8
  MANIFEST_FILE="$TMP_DIR/update-manifest.json"
  if ! download_file "$dl_root/update-manifest.json" "$MANIFEST_FILE"; then
    die "下载更新清单失败: $dl_root/update-manifest.json" 1
  fi
  if [ ! -s "$MANIFEST_FILE" ]; then
    die "更新清单为空" 1
  fi
  remotev=$(manifest_field "$MANIFEST_FILE" version)
  remotev=${remotev#v}
  if [ -z "$remotev" ]; then
    die "更新清单缺少 version" 1
  fi
  update_file=$(manifest_field "$MANIFEST_FILE" file)
  if [ -z "$update_file" ]; then
    die "更新清单缺少 file" 1
  fi
  case "$update_file" in
    */*|..|.) die "更新清单 file 非法: $update_file" 1 ;;
  esac
  expected_sha=$(manifest_field "$MANIFEST_FILE" sha256)
  if [ -z "$expected_sha" ]; then
    die "更新清单缺少 sha256" 1
  fi
  if [ "${#expected_sha}" -ne 64 ]; then
    die "更新清单 sha256 格式非法" 1
  fi
  update_notes=$(manifest_notes "$MANIFEST_FILE")
  if [ -z "$update_notes" ]; then
    die "更新清单缺少 notes" 1
  fi
  MANIFEST_FILES_REL="$TMP_DIR/manifest-files.rel"
  manifest_files "$MANIFEST_FILE" > "$MANIFEST_FILES_REL"
  if [ ! -s "$MANIFEST_FILES_REL" ]; then
    die "更新清单缺少 files" 1
  fi
  validate_manifest_files "$MANIFEST_FILE"
  localv=$(local_version "$update_prefix")
  if version_gt "$remotev" "$localv"; then
    update_status=update
  elif [ "$remotev" = "$localv" ]; then
    update_status=current
  else
    update_status=downgrade
  fi
  if [ "$update_check" -eq 1 ]; then
    if [ "$update_status" = "update" ]; then
      info "有可用更新: v$localv -> v$remotev"
      info "更新说明: $update_notes"
      return 0
    fi
    info "当前已是最新版本: v$localv"
    return 1
  fi
  if [ "$update_status" = "current" ] && [ "$UPDATE_FORCE" -ne 1 ]; then
    info "已是最新版本: v$localv"
    return 0
  fi
  if [ "$update_status" = "downgrade" ] && [ "$UPDATE_FORCE" -ne 1 ]; then
    die "远端版本较低 (v$remotev < v$localv)，使用 --force 允许降级" 2
  fi
  if [ "$update_dry" -eq 1 ]; then
    printf '更新计划:\n'
    printf '  下载根: %s\n' "$dl_root"
    printf '  本地版本: v%s\n' "$localv"
    printf '  远端版本: v%s\n' "$remotev"
    printf '  状态: %s\n' "$update_status"
    printf '  安装前缀: %s\n' "$update_prefix"
    printf '  更新说明: %s\n' "$update_notes"
    if [ -n "$update_shell" ]; then
      printf '  Shell: %s\n' "$update_shell"
    fi
    printf '  (dry-run，未写入任何文件)\n'
    return 0
  fi
  if [ "$update_status" = "update" ]; then
    info "更新说明: $update_notes"
  fi
  if [ "$update_yes" -ne 1 ] && [ -t 0 ]; then
    printf '更新 ngc v%s -> v%s？[y/N] ' "$localv" "$remotev" >&2
    if ! read -r ans; then
      ans=""
    fi
    case "$ans" in
      y|Y) ;;
      *) info "已取消"; return 0 ;;
    esac
  fi
  tar_file="$TMP_DIR/$update_file"
  if ! download_file "$dl_root/$update_file" "$tar_file"; then
    die "下载归档失败: $dl_root/$update_file" 1
  fi
  actual=$(sha256_of "$tar_file")
  if [ "$expected_sha" != "$actual" ]; then
    rm -f -- "$tar_file"
    die "sha256 校验失败" 1
  fi
  if ! tar -xzf "$tar_file" -C "$TMP_DIR"; then
    die "解压失败" 1
  fi
  if [ ! -f "$TMP_DIR/install.sh" ]; then
    die "归档中缺少 install.sh" 1
  fi
  if [ -n "${ZSH_VERSION:-}" ]; then
    self_shell=zsh
  elif [ -n "${BASH_VERSION:-}" ]; then
    self_shell=bash
  else
    self_shell=bash
  fi
  record_file="$update_prefix/share/$NGC_PROJECT/install-record"
  OLD_RECORD="$TMP_DIR/old-install-record"
  if [ -f "$record_file" ]; then
    cp -- "$record_file" "$OLD_RECORD" || die "无法读取安装记录" 8
  fi
  install_args=(--prefix "$update_prefix")
  if [ -n "$update_shell" ] && [ "$update_shell" != "auto" ]; then
    install_args+=(--shell "$update_shell")
  fi
  if [ "$update_status" != "update" ]; then
    install_args+=(--reinstall)
  fi
  if [ "$UPDATE_FORCE" -eq 1 ]; then
    install_args+=(--force)
  fi
  "$self_shell" "$TMP_DIR/install.sh" ${install_args[@]+"${install_args[@]}"} < /dev/null

  if [ -f "$OLD_RECORD" ]; then
    while IFS= read -r old_path; do
      case "$old_path" in
        ''|'#'*) continue ;;
      esac
      case "$old_path" in
        "$update_prefix"/*) old_rel=${old_path#"$update_prefix"/} ;;
        *)
          log "存疑，未删除（请人工确认）: $old_path"
          continue
          ;;
      esac
      if grep -Fxq -- "$old_rel" "$MANIFEST_FILES_REL"; then
        continue
      fi
      if [ -f "$old_path" ] || [ -L "$old_path" ]; then
        if ! rm -f -- "$old_path"; then
          die "删除旧版文件失败: $old_path" 8
        fi
        if ! rmdir -- "$(dirname -- "$old_path")" 2>/dev/null; then
          log "提示: 目录未清空或无法删除，已保留: $(dirname -- "$old_path")"
        fi
      fi
    done < "$OLD_RECORD"
  fi
  rm -f -- "$NGC_STATE_DIR/update-files" "$NGC_STATE_DIR/path-map"

  printf '更新完成: v%s -> v%s\n' "$localv" "$remotev"
  printf '请重启或重新加载 shell（如 source ~/.bashrc）以使更新生效。\n'
}

cmd_complete() {
  previous=""
  last_word=""
  for word in "$@"; do
    previous=$last_word
    last_word=$word
  done
  if [ "$previous" = "--shell" ]; then
    printf '%s\n' bash zsh auto
    return 0
  fi
  first=""
  if [ $# -gt 0 ]; then
    first=$1
  fi
  case "$first" in
    update)
      printf '%s\n' -y --yes --check --force --dry-run --base-url --insecure --prefix --shell
      return 0
      ;;
    toggle)
      printf '%s\n' --force --yes
      return 0
      ;;
    list|edit|delete)
      if [ -d "$NGC_ROOT" ]; then
        TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ngc-complete.XXXXXX")
        out="$TMP_DIR/paths"
        : > "$out"
        find "$NGC_ROOT" -type f \( -name '*.conf' -o -name '*.conf.disabled' \) 2>/dev/null >> "$out" || true
        if [ -d "$NGC_ROOT/sites-available" ]; then
          find "$NGC_ROOT/sites-available" -maxdepth 1 -type f 2>/dev/null >> "$out" || true
        fi
        LC_ALL=C sort -u "$out" | while IFS= read -r p; do
          printf '%s\n' "${p#"$NGC_ROOT"/}"
        done
      fi
      return 0
      ;;
    '')
      printf '%s\n' list toggle edit delete check reload update version help
      return 0
      ;;
    -*)
      printf '%s\n' --root --nginx --conf --force --yes --help --version
      return 0
      ;;
  esac
  printf '%s\n' list toggle edit delete check reload update version help
}

main() {
  load_config
  while [ $# -gt 0 ]; do
    case "$1" in
      --root)
        [ $# -ge 2 ] || die "--root 需要参数" 2
        NGC_ROOT=$2
        shift 2
        ;;
      --root=*) NGC_ROOT=${1#*=}; shift ;;
      --nginx)
        [ $# -ge 2 ] || die "--nginx 需要参数" 2
        NGC_NGINX_BIN=$2
        shift 2
        ;;
      --nginx=*) NGC_NGINX_BIN=${1#*=}; shift ;;
      --conf)
        [ $# -ge 2 ] || die "--conf 需要参数" 2
        NGC_NGINX_CONF=$2
        CONF_DEFAULT=0
        shift 2
        ;;
      --conf=*)
        NGC_NGINX_CONF=${1#*=}
        CONF_DEFAULT=0
        shift
        ;;
      --force) FORCE=1; shift ;;
      --yes) FORCE=1; YES_FLAG=1; shift ;;
      -h|--help) print_help; return 0 ;;
      -V|--version) cmd_version; return 0 ;;
      --) shift; break ;;
      -*) die "未知参数: $1" 2 ;;
      *) break ;;
    esac
  done
  if [ "$CONF_DEFAULT" -eq 1 ]; then
    NGC_NGINX_CONF="$NGC_ROOT/nginx.conf"
  fi
  if [ $# -eq 0 ]; then
    print_help
    return 2
  fi
  sub=$1
  shift
  case "$sub" in
    list) cmd_list "$@" ;;
    toggle) cmd_toggle "$@" ;;
    edit) cmd_edit "$@" ;;
    delete) cmd_delete "$@" ;;
    check) cmd_check "$@" ;;
    reload) cmd_reload "$@" ;;
    update) cmd_update "$@" ;;
    version) cmd_version ;;
    help) cmd_help ;;
    __complete) cmd_complete "$@" ;;
    *) die "未知命令: $sub" 2 ;;
  esac
}

main "$@"
