#!/usr/bin/env bash
# Nexus 6.0 — 1Panel + Docker 一键安装 / 升级（参数已全部内置，无需 export）
#
# 新机 1Panel 终端一条命令:
#   curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/nexus-1panel.sh" | bash -s install
#
# 已 clone 到 /opt/nexus:
#   cd /opt/nexus && bash deploy/nexus-1panel.sh install --skip-clone
#
# 其他:
#   bash deploy/nexus-1panel.sh install --profile 4c16g
#   bash deploy/nexus-1panel.sh upgrade
#   bash deploy/nexus-1panel.sh check
#   bash deploy/nexus-1panel.sh ports
#
# 公共仓库匿名 clone，无需 Gitea 令牌。
# 仅私人仓库可选: deploy/nexus-1panel.secrets.sh 或 /root/.nexus-deploy.env
# 或 clone 时 URL 带令牌，脚本会从 git remote 自动解析

set -euo pipefail

# ═══════════════════════════════════════════════════════════════════
# 内置路径/域名（无敏感信息，可提交 Git）
# ═══════════════════════════════════════════════════════════════════
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
NEXUS_FROM_ENV="${NEXUS_FROM_ENV:-/www/wwwroot/api.synaglobal.vip/.env}"
NEXUS_BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
NEXUS_DEPLOY_ENV="${NEXUS_DEPLOY_ENV:-/root/.nexus-deploy.env}"
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"

COMPOSE_FILE="docker/docker-compose.prod.yml"
COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
ENV_PROD="docker/.env.prod"
HOST_PROFILE_FILE="docker/.host-profile"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step()  { echo -e "${CYAN}[STEP]${NC} $*"; }

SKIP_CLONE=false
FRESH_INSTALL=false
REUSE_SECRETS=false
FROM_ENV="${NEXUS_FROM_ENV}"
NO_BACKUP=false
REQUIRE_BACKUP=false
PRUNE_IMAGES=false
NO_CACHE=false
CHECK_ONLY=false
DRY_RUN=false
CUSTOM_CPUS=""
CUSTOM_MEM_GB=""

usage() {
    cat <<'EOF'
Nexus 1Panel 一键脚本

  bash deploy/nexus-1panel.sh install [选项]     克隆/配置/启动
  bash deploy/nexus-1panel.sh upgrade [选项]     拉代码+备份+重建
  bash deploy/nexus-1panel.sh check              检查远程是否有更新
  bash deploy/nexus-1panel.sh ports              检查 Gitea/8600/80/443 与防火墙
  bash deploy/nexus-1panel.sh init-token <令牌>  覆盖内置 Gitea 令牌（一般不需要）

资源档位（三选一）:
  --profile 2c8g|4c16g|1c4g    预设（默认 2c8g = 2核8G）
  --cpus N --mem-gb N           自动匹配最近预设

安装选项:
  --skip-clone                  已在 /opt/nexus，仅 pull
  --fresh                       全新安装（本机自动生成唯一密钥 + /app/install.html）
  --from-env PATH               从旧 .env 迁机导入密钥
  --reuse-secrets               使用 secrets 文件中的 NEXUS 密钥（勿用于新机）
  --domain NAME                 覆盖域名

升级选项:
  --no-backup  --require-backup  --prune  --no-cache  --dry-run  --check（仅查 Git，不重建）

Git: 公共仓库匿名 clone（无需令牌）；私人仓库见 init-token
EOF
    exit 0
}

script_dir() {
    dirname "$(readlink -f "${BASH_SOURCE[0]}")"
}

secrets_file_path() {
    echo "$(script_dir)/nexus-1panel.secrets.sh"
}

load_token_from_git_remote() {
    local root="${1:-$NEXUS_ROOT}"
    [[ -n "${NEXUS_GITEA_TOKEN:-}" ]] && return 0
    [[ -d "${root}/.git" ]] || return 0
    local url
    url="$(git -C "$root" remote get-url origin 2>/dev/null || true)"
    if [[ "$url" =~ http://[^:]+:([^@]+)@ ]]; then
        NEXUS_GITEA_TOKEN="${BASH_REMATCH[1]}"
        info "已从 git remote 解析 Gitea 令牌"
    fi
}

load_secrets() {
    local secrets_file
    secrets_file="$(secrets_file_path)"
    if [[ -f "$secrets_file" ]]; then
        # shellcheck disable=SC1090
        set -a
        source "$secrets_file"
        set +a
        info "已加载 $secrets_file"
    fi
    if [[ -f "$NEXUS_DEPLOY_ENV" ]]; then
        # shellcheck disable=SC1090
        set -a
        source "$NEXUS_DEPLOY_ENV"
        set +a
        info "已加载 $NEXUS_DEPLOY_ENV"
    fi
    load_token_from_git_remote "$NEXUS_ROOT"
    if [[ "$FRESH_INSTALL" == true ]]; then
        unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
        info "全新安装：忽略 secrets 中的 NEXUS 密钥，将为本机自动生成"
    fi
}

resolve_profile() {
    if [[ -n "$CUSTOM_CPUS" && -n "$CUSTOM_MEM_GB" ]]; then
        local c m
        c="$CUSTOM_CPUS"
        m="$CUSTOM_MEM_GB"
        if [[ "$c" -le 1 && "$m" -le 4 ]]; then
            NEXUS_PROFILE="1c4g"
        elif [[ "$c" -le 2 && "$m" -le 8 ]]; then
            NEXUS_PROFILE="2c8g"
        else
            NEXUS_PROFILE="4c16g"
        fi
        info "规格 ${c}C${m}G → 档位 ${NEXUS_PROFILE}"
        return 0
    fi
    case "$NEXUS_PROFILE" in
        1c4g|2c8g|4c16g) ;;
        *)
            error "未知档位: $NEXUS_PROFILE（可用: 1c4g, 2c8g, 4c16g）"
            exit 1
            ;;
    esac
}

profile_env_file() {
    local root="$1"
    echo "$root/docker/profiles/${NEXUS_PROFILE}.env"
}

save_host_profile() {
    local root="$1"
    echo "$NEXUS_PROFILE" >"$root/$HOST_PROFILE_FILE"
}

load_saved_profile() {
    local root="$1"
    if [[ -f "$root/$HOST_PROFILE_FILE" ]]; then
        NEXUS_PROFILE="$(tr -d '\r\n' <"$root/$HOST_PROFILE_FILE")"
        info "使用已保存档位: $NEXUS_PROFILE"
    fi
}

apply_pool_to_env_prod() {
    local root="$1" prof="$2"
    local env_file="$root/$ENV_PROD"
    [[ -f "$env_file" ]] || return 0
    local pool overflow
    pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
    overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
    [[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
    [[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
}

has_1panel_network() {
    docker network inspect 1panel-network >/dev/null 2>&1
}

upsert_env_var() {
    local file="$1" key="$2" val="$3"
    if grep -qE "^${key}=" "$file" 2>/dev/null; then
        sed -i "s|^${key}=.*|${key}=${val}|" "$file"
    else
        echo "${key}=${val}" >>"$file"
    fi
}

migrate_redis_url_host() {
    local old_url="$1" new_host="$2"
    python3 - "$old_url" "$new_host" <<'PY'
import sys
from urllib.parse import quote, unquote, urlparse

old_url, new_host = sys.argv[1], sys.argv[2]
u = urlparse(old_url)
if u.scheme != "redis":
    sys.exit(0)
password = unquote(u.password or "")
if not password and u.username:
    password = unquote(u.username)
if password and new_host:
    print(f"redis://:{quote(password, safe='')}@{new_host}:6379/0")
PY
}

# Prefer wizard-written redis://:pass@host from nexus-state or .env.prod; avoid clobbering on nx update.
resolve_nexus_redis_url() {
    local redis_host="$1"
    local env_file="$2"
    local vol_url="" existing="" migrated="" host_lc="${redis_host,,}"

    local state_vol
    state_vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
    if [[ -n "$state_vol" ]]; then
        vol_url="$(docker run --rm -v "${state_vol}:/data" alpine sh -c \
            "grep '^NEXUS_REDIS_URL=' /data/.env 2>/dev/null | cut -d= -f2- | tr -d '\"'" 2>/dev/null || true)"
        if [[ -n "$vol_url" ]]; then
            if [[ "${vol_url,,}" == *"${host_lc}"* ]]; then
                echo "$vol_url"
                return 0
            fi
            if [[ "$vol_url" == *"@"* ]]; then
                migrated="$(migrate_redis_url_host "$vol_url" "$redis_host" 2>/dev/null || true)"
                if [[ -n "$migrated" ]]; then
                    echo "$migrated"
                    return 0
                fi
            fi
        fi
    fi
    if [[ -f "$env_file" ]]; then
        existing="$(grep '^NEXUS_REDIS_URL=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
        if [[ -n "$existing" && "$existing" == *"@"* ]]; then
            if [[ "${existing,,}" == *"${host_lc}"* ]]; then
                echo "$existing"
                return 0
            fi
            migrated="$(migrate_redis_url_host "$existing" "$redis_host" 2>/dev/null || true)"
            if [[ -n "$migrated" ]]; then
                echo "$migrated"
                return 0
            fi
        fi
    fi
    echo "redis://${redis_host}:6379/0"
}

nexus_container_name() {
    docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
}

nexus_publish_port() {
    local root="$1"
    local env_file="$root/$ENV_PROD"
    local port
    port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
    echo "${port:-8600}"
}

nexus_container_has_install_lock() {
    local cname="$1"
    docker exec "$cname" test -f /app/.install_locked 2>/dev/null \
        || docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null
}

restore_install_lock_in_container() {
    local cname="$1"
    [[ -n "$cname" ]] || return 0
    docker exec "$cname" sh -c '
        if [ ! -f /app/.install_locked ] && [ -f /var/lib/nexus/.install_locked ]; then
            cp /var/lib/nexus/.install_locked /app/.install_locked
            chmod 600 /app/.install_locked
        fi
    ' 2>/dev/null || true
}

nexus_on_1panel_network() {
    local cname="$1"
    [[ -n "$cname" ]] || return 1
    docker inspect "$cname" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null \
        | grep -qw '1panel-network'
}

verify_nexus_1panel_network() {
    local root="$1"
    if ! has_1panel_network; then
        return 0
    fi
    local cname
    cname="$(nexus_container_name)"
    [[ -n "$cname" ]] || return 0
    restore_install_lock_in_container "$cname"
    if nexus_on_1panel_network "$cname"; then
        info "已验证 Nexus 接入 1panel-network"
        return 0
    fi
    error "Nexus 容器 ${cname} 未接入 1panel-network，无法解析 1Panel MySQL/Redis 主机名"
    warn "正在叠加 docker-compose.1panel.yml 重新创建容器..."
    compose_cmd "$root" up -d --no-build --remove-orphans
    sleep 2
    cname="$(nexus_container_name)"
    restore_install_lock_in_container "$cname"
    if nexus_on_1panel_network "$cname"; then
        info "已修复：Nexus 已接入 1panel-network"
        return 0
    fi
    error "修复失败：Nexus 仍未接入 1panel-network"
    return 1
}

set_install_complete() {
    local root="$1"
    local env_file="$root/$ENV_PROD"
    local cname
    [[ -f "$env_file" ]] || return 0
    cname="$(nexus_container_name)"
    [[ -n "$cname" ]] || return 0
    restore_install_lock_in_container "$cname"
    if nexus_container_has_install_lock "$cname"; then
        archive_install_wizard_in_container "$cname"
        upsert_env_var "$env_file" "NEXUS_INSTALL_WIZARD_PENDING" "0"
        info "安装已完成：NEXUS_INSTALL_WIZARD_PENDING=0"
    fi
}

sync_env_prod_to_volume() {
    local root="$1"
    local env_file="$root/$ENV_PROD"
    local state_vol cname
    state_vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
    [[ -n "$state_vol" && -f "$env_file" ]] || return 0
    cname="$(nexus_container_name)"
    [[ -n "$cname" ]] || return 0
    restore_install_lock_in_container "$cname"
    nexus_container_has_install_lock "$cname" || return 0
    python3 - "$env_file" "$state_vol" <<'PY'
import re
import subprocess
import sys
from pathlib import Path

env_file = Path(sys.argv[1])
vol = sys.argv[2]
keys = (
    "NEXUS_SECRET_KEY",
    "NEXUS_API_KEY",
    "NEXUS_ENCRYPTION_KEY",
    "NEXUS_REDIS_URL",
    "NEXUS_DATABASE_URL",
)
prod = {}
for line in env_file.read_text(encoding="utf-8").splitlines():
    m = re.match(r"^([A-Z0-9_]+)=(.*)$", line.strip())
    if m and m.group(1) in keys and m.group(2).strip():
        prod[m.group(1)] = m.group(2).strip().strip('"')
if not prod:
    sys.exit(0)
cur = subprocess.run(
    ["docker", "run", "--rm", "-v", f"{vol}:/data", "alpine", "cat", "/data/.env"],
    capture_output=True,
    text=True,
)
text = cur.stdout if cur.returncode == 0 else ""
if not text.strip():
    sys.exit(0)
lines = text.splitlines()
out = []
seen = set()
for line in lines:
    key = line.split("=", 1)[0].strip() if "=" in line else ""
    if key in prod:
        out.append(f'{key}={prod[key]}')
        seen.add(key)
    else:
        out.append(line)
for key, val in prod.items():
    if key not in seen:
        out.append(f'{key}={val}')
new_text = "\n".join(out).rstrip() + "\n"
subprocess.run(
    ["docker", "run", "--rm", "-i", "-v", f"{vol}:/data", "alpine", "sh", "-c", "cat > /data/.env && chmod 600 /data/.env"],
    input=new_text,
    text=True,
    check=True,
)
print("synced")
PY
    info "已将 docker/.env.prod 密钥/URL 同步至 nexus-state 卷"
}

tag_rollback_image() {
    local root="$1" old_id
    old_id="$(docker ps --filter "name=nexus-prod-nexus" --format '{{.Image}}' 2>/dev/null | head -1 || true)"
    if [[ -z "$old_id" ]]; then
        old_id="$(compose_cmd "$root" images -q nexus 2>/dev/null | head -1 || true)"
    fi
    if [[ -n "$old_id" ]]; then
        docker tag "$old_id" "nexus-prod-nexus:rollback" 2>/dev/null || true
        info "已标记回滚镜像 nexus-prod-nexus:rollback"
    fi
}

gate_new_image_import() {
    local root="$1" img
    img="$(compose_cmd "$root" images -q nexus 2>/dev/null | head -1 || true)"
    if [[ -z "$img" ]]; then
        error "镜像门控失败：未找到 nexus 镜像"
        return 1
    fi
    if ! docker run --rm --entrypoint python3 "$img" -c "import server.main" >/dev/null 2>&1; then
        error "镜像门控失败：import server.main"
        return 1
    fi
    info "镜像门控通过 (import server.main)"
    return 0
}

rollback_compose_upgrade() {
    local root="$1"
    if docker image inspect nexus-prod-nexus:rollback >/dev/null 2>&1; then
        warn "健康检查失败，回滚至上一镜像..."
        docker tag nexus-prod-nexus:rollback nexus-prod-nexus:latest 2>/dev/null || true
        compose_cmd "$root" up -d --no-build --remove-orphans
        if ! verify_nexus_1panel_network "$root"; then
            error "回滚后仍未接入 1panel-network"
            return 1
        fi
        return 0
    fi
    warn "无回滚镜像 nexus-prod-nexus:rollback，跳过自动回滚"
    return 1
}

write_1panel_hosts_volume() {
    local mysql_host="${1:-}" redis_host="${2:-}"
    [[ -n "$mysql_host" || -n "$redis_host" ]] || return 0
    local vol json
    vol="$(docker volume ls -q --filter name=nexus-web-data 2>/dev/null | head -1 || true)"
    if [[ -z "$vol" ]]; then
        warn "未找到 nexus-web-data 卷，跳过写入 1panel-hosts.json"
        return 0
    fi
    json="{"
    if [[ -n "$mysql_host" ]]; then
        json+="\"db_host\":\"${mysql_host}\""
        [[ -n "$redis_host" ]] && json+=","
    fi
    if [[ -n "$redis_host" ]]; then
        json+="\"redis_host\":\"${redis_host}\""
    fi
    json+="}"
    if printf '%s' "$json" | docker run --rm -i -v "${vol}:/data" alpine:3.19 \
        sh -c 'cat > /data/1panel-hosts.json && chmod 644 /data/1panel-hosts.json'; then
        info "已写入卷 ${vol} → web/data/1panel-hosts.json"
    else
        warn "写入 1panel-hosts.json 失败（向导仍可读 .env.prod 环境变量）"
    fi
}

sync_1panel_service_hosts() {
    local root="$1"
    local env_file="$root/$ENV_PROD"
    local detect="$root/deploy/detect-1panel-services.sh"
    [[ -f "$env_file" ]] || return 0
    if ! has_1panel_network; then
        warn "未找到 1panel-network — 请先在 1Panel 应用商店安装 MySQL/Redis"
        return 0
    fi
    if [[ ! -x "$detect" ]]; then
        chmod +x "$detect" 2>/dev/null || true
    fi
    if [[ ! -x "$detect" ]]; then
        warn "未找到 detect-1panel-services.sh，跳过 1Panel 容器名探测"
        return 0
    fi
    local line mysql_host="" redis_host="" detect_out
    if ! detect_out="$("$detect" 2>&1)"; then
        warn "1Panel 容器探测: ${detect_out:-无 MySQL/Redis 容器运行}"
        return 0
    fi
    while IFS= read -r line; do
        case "$line" in
            NEXUS_1PANEL_DB_HOST=*)
                mysql_host="${line#NEXUS_1PANEL_DB_HOST=}"
                upsert_env_var "$env_file" NEXUS_1PANEL_DB_HOST "$mysql_host"
                info "1Panel MySQL 容器: $mysql_host（安装向导将预填为数据库主机）"
                ;;
            NEXUS_1PANEL_REDIS_HOST=*)
                redis_host="${line#NEXUS_1PANEL_REDIS_HOST=}"
                upsert_env_var "$env_file" NEXUS_1PANEL_REDIS_HOST "$redis_host"
                local redis_url
                redis_url="$(resolve_nexus_redis_url "$redis_host" "$env_file")"
                upsert_env_var "$env_file" "NEXUS_REDIS_URL" "$redis_url"
                if [[ "$redis_url" == *"@"* ]]; then
                    info "1Panel Redis 容器: $redis_host（NEXUS_REDIS_URL 已保留向导密码）"
                else
                    info "1Panel Redis 容器: $redis_host（NEXUS_REDIS_URL 占位，向导步骤 3 填密码后写入卷 .env）"
                fi
                ;;
        esac
    done <<<"$detect_out"
    write_1panel_hosts_volume "$mysql_host" "$redis_host"
}

compose_cmd() {
    local root="$1"
    shift
    local prof
    prof="$(profile_env_file "$root")"
    if [[ ! -f "$prof" ]]; then
        error "缺少档位文件: $prof"
        exit 1
    fi
    cd "$root"
    local -a compose_args=(-f "$COMPOSE_FILE")
    if has_1panel_network && [[ -f "$COMPOSE_1PANEL" ]]; then
        compose_args+=(-f "$COMPOSE_1PANEL")
    fi
    docker compose "${compose_args[@]}" --env-file "$ENV_PROD" --env-file "$prof" "$@"
}

rand_secret() {
    openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'
}

rand_pass() {
    openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'
}

read_env_val() {
    local file="$1" key="$2"
    grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}

parse_gitea_endpoint() {
    local hp="${NEXUS_GITEA_HOST}"
    GITEA_HOST="${hp%%:*}"
    GITEA_PORT="${hp##*:}"
    if [[ "$GITEA_HOST" == "$GITEA_PORT" || -z "$GITEA_PORT" ]]; then
        GITEA_PORT=3000
    fi
}

nexus_publish_port() {
    local root="${1:-}"
    local p="${NEXUS_PUBLISH_PORT:-8600}"
    if [[ -n "$root" && -f "$root/$ENV_PROD" ]]; then
        local v
        v="$(read_env_val "$root/$ENV_PROD" NEXUS_PUBLISH_PORT)"
        [[ -n "$v" ]] && p="$v"
    fi
    echo "$p"
}

tcp_reachable() {
    local host="$1" port="$2" t="${3:-3}"
    if command -v nc >/dev/null 2>&1; then
        nc -z -w "$t" "$host" "$port" 2>/dev/null
        return $?
    fi
    if command -v timeout >/dev/null 2>&1; then
        timeout "$t" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
            && { exec 3<&- 3>&-; return 0; }
    else
        bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
            && { exec 3<&- 3>&-; return 0; }
    fi
    return 1
}

local_port_listening() {
    local port="$1"
    if command -v ss >/dev/null 2>&1; then
        ss -tlnH 2>/dev/null | grep -qE ":${port}[[:space:]]"
        return $?
    fi
    if command -v netstat >/dev/null 2>&1; then
        netstat -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"
        return $?
    fi
    curl -sf --max-time 2 "http://127.0.0.1:${port}/health" >/dev/null 2>&1
}

firewall_may_block_port() {
    local port="$1"
    if command -v ufw >/dev/null 2>&1; then
        local st
        st="$(ufw status 2>/dev/null | head -1 || true)"
        if [[ "$st" == *"active"* ]]; then
            ufw status 2>/dev/null | grep -qE "${port}/tcp[[:space:]]+ALLOW" && return 1
            return 0
        fi
    fi
    if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -qi running; then
        firewall-cmd --list-ports 2>/dev/null | grep -q "${port}/tcp" && return 1
        firewall-cmd --list-services 2>/dev/null | grep -qE '^(http|https)$' \
            && [[ "$port" == "80" || "$port" == "443" ]] && return 1
        return 0
    fi
    return 1
}

remind_firewall() {
    local port="$1" label="$2"
    if firewall_may_block_port "$port"; then
        warn "防火墙可能未放行 ${port}/tcp（${label}）"
        warn "  本机: ufw allow ${port}/tcp  或 1Panel → 主机 → 防火墙"
        warn "  云厂商控制台: 安全组入站放行 ${port}/tcp"
    fi
}

check_ports_preflight() {
    local root="${1:-$NEXUS_ROOT}"
    local nport issues=0
    parse_gitea_endpoint
    nport="$(nexus_publish_port "$root")"

    step "检查端口与防火墙..."
    if tcp_reachable "$GITEA_HOST" "$GITEA_PORT"; then
        info "Gitea ${GITEA_HOST}:${GITEA_PORT} — 可达（git 拉取）"
    else
        warn "Gitea ${GITEA_HOST}:${GITEA_PORT} — 无法连接，clone/pull 会失败"
        warn "  请确认 Gitea 在线，且本机出站与该端口未被防火墙/安全组拦截"
        issues=$((issues + 1))
    fi

    if local_port_listening "$nport"; then
        info "127.0.0.1:${nport} — 已有监听（可能为旧 Nexus 实例）"
    else
        info "127.0.0.1:${nport} — 尚未监听（安装后将由 Compose 绑定）"
    fi

    for spec in "80:HTTP" "443:HTTPS"; do
        local p="${spec%%:*}" label="${spec##*:}"
        if local_port_listening "$p"; then
            info "本机 :${p} — 已监听（${label} / 1Panel）"
        else
            warn "本机 :${p} — 未监听，外网 ${label} 暂不可用"
            warn "  安装完成后: 1Panel → 网站 → 反代 http://127.0.0.1:${nport} 并申请 SSL"
            issues=$((issues + 1))
        fi
        remind_firewall "$p" "${label} 公网"
    done

    remind_firewall 22 "SSH（远程维护）"

    if [[ "$issues" -gt 0 ]]; then
        warn "共 ${issues} 项端口提醒（可继续安装，但请先处理 Gitea 不可达问题）"
    else
        info "端口预检通过"
    fi
    echo ""
}

check_ports_post() {
    local root="$1"
    local nport health_ext
    nport="$(nexus_publish_port "$root")"

    step "检查服务端口..."
    if local_port_listening "$nport"; then
        info "Nexus 127.0.0.1:${nport} — 已监听"
    else
        warn "127.0.0.1:${nport} — 未监听，请执行: cd $root && docker compose ... ps"
    fi

    if local_port_listening 443; then
        health_ext="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${NEXUS_DOMAIN}/health" 2>/dev/null || echo "000")"
        if [[ "$health_ext" == "200" ]]; then
            info "https://${NEXUS_DOMAIN}/health — 外网可达 (HTTP 200)"
        else
            warn "https://${NEXUS_DOMAIN}/health — 返回 HTTP ${health_ext}"
            warn "  若外网打不开: 1Panel 反代是否指向 127.0.0.1:${nport}，安全组是否放行 443"
        fi
    else
        warn "本机 443 未监听 — 请在 1Panel 创建站点并启用 HTTPS，否则浏览器无法访问 ${NEXUS_DOMAIN}"
        remind_firewall 443 "HTTPS"
    fi

    if ! local_port_listening 80; then
        info "本机 80 未监听（仅 HTTPS 时正常）"
    fi
    echo ""
}

cmd_ports() {
    require_root
    check_ports_preflight "$NEXUS_ROOT"
    if [[ -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
        check_ports_post "$NEXUS_ROOT"
    fi
}

require_root() {
    if [[ "$(id -u)" -ne 0 ]]; then
        error "请切换到 root 执行（su -），不要用 sudo 包一层"
        exit 1
    fi
}

require_token() {
    if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
        return 0
    fi
    info "未配置 Gitea 令牌，按公共仓库匿名 clone/pull"
}

git_remote_url() {
    if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
        echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
    else
        echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
    fi
}

ensure_packages() {
    if command -v apt-get >/dev/null 2>&1; then
        export DEBIAN_FRONTEND=noninteractive
        apt-get update -qq
        apt-get install -y -qq git curl openssl ca-certificates >/dev/null 2>&1 || true
    fi
}

ensure_docker() {
    if docker compose version >/dev/null 2>&1; then
        info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version)"
        return 0
    fi
    error "未检测到 docker compose。请在 1Panel → 容器 安装 Docker。"
    exit 1
}

clone_or_pull() {
    local url
    url="$(git_remote_url)"

    if [[ "$SKIP_CLONE" == true ]]; then
        if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
            error "--skip-clone 但 $NEXUS_ROOT 不是 git 仓库"
            exit 1
        fi
        info "在 $NEXUS_ROOT 执行 git pull..."
        git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
        git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
        git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
        return 0
    fi

    mkdir -p "$(dirname "$NEXUS_ROOT")"
    if [[ -d "$NEXUS_ROOT/.git" ]]; then
        info "仓库已存在，同步 origin/${GIT_BRANCH}..."
        git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
        git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
        git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
        return 0
    fi

    if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
        error "目录非空且不是 git 仓库: $NEXUS_ROOT"
        exit 1
    fi

    rm -rf "$NEXUS_ROOT"
    info "克隆 Nexus → $NEXUS_ROOT ..."
    git clone "$url" "$NEXUS_ROOT"
    git -C "$NEXUS_ROOT" checkout "$GIT_BRANCH" 2>/dev/null || true
}

write_temp_secrets_env() {
    local f
    f="$(mktemp /tmp/nexus-secrets-import.XXXXXX.env)"
    {
        printf 'NEXUS_SECRET_KEY=%s\n' "$NEXUS_SECRET_KEY"
        printf 'NEXUS_API_KEY=%s\n' "$NEXUS_API_KEY"
        printf 'NEXUS_ENCRYPTION_KEY=%s\n' "$NEXUS_ENCRYPTION_KEY"
    } >"$f"
    chmod 600 "$f"
    echo "$f"
}

write_env_prod() {
    local root="$1"
    local env_file="$root/$ENV_PROD"
    local example="$root/docker/.env.prod.example"
    local gen="$root/scripts/generate_nexus_secrets.py"
    local prof backup tmp

    if [[ ! -f "$example" ]]; then
        error "缺少 $example"
        exit 1
    fi
    if [[ ! -f "$gen" ]]; then
        error "缺少 $gen"
        exit 1
    fi

    if [[ -f "$env_file" ]]; then
        warn "$env_file 已存在，仅更新域名与连接池"
        sed -i "s|^NEXUS_CORS_ORIGINS=.*|NEXUS_CORS_ORIGINS=https://${NEXUS_DOMAIN}|" "$env_file"
        sed -i "s|^NEXUS_API_BASE_URL=.*|NEXUS_API_BASE_URL=https://${NEXUS_DOMAIN}|" "$env_file"
        apply_pool_to_env_prod "$root" "$(profile_env_file "$root")"
        chmod 600 "$env_file"
        return 0
    fi

    prof="$(profile_env_file "$root")"
    backup="/root/.nexus-install-secrets-$(date +%Y%m%d%H%M%S).env"

    if [[ -n "$FROM_ENV" && -f "$FROM_ENV" ]]; then
        info "迁机：从 $FROM_ENV 导入密钥..."
        python3 "$gen" write-prod \
            --template "$example" \
            --output "$env_file" \
            --domain "$NEXUS_DOMAIN" \
            --profile-env "$prof" \
            --from-env "$FROM_ENV" \
            --backup "$backup"
    elif [[ "$FRESH_INSTALL" == true ]]; then
        info "全新安装：为本机生成唯一 NEXUS 密钥（MySQL/Redis 请自行安装）..."
        python3 "$gen" write-prod \
            --template "$example" \
            --output "$env_file" \
            --domain "$NEXUS_DOMAIN" \
            --profile-env "$prof" \
            --fresh \
            --wizard-pending \
            --backup "$backup"
        info "密钥备份: $backup（仅本机，勿复制到其他服务器）"
        info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
    elif [[ "$REUSE_SECRETS" == true && -n "${NEXUS_SECRET_KEY:-}" && -n "${NEXUS_API_KEY:-}" && -n "${NEXUS_ENCRYPTION_KEY:-}" ]]; then
        warn "使用 secrets 中的 NEXUS 密钥（仅迁机/复用，新机勿用 --reuse-secrets）"
        tmp="$(write_temp_secrets_env)"
        python3 "$gen" write-prod \
            --template "$example" \
            --output "$env_file" \
            --domain "$NEXUS_DOMAIN" \
            --profile-env "$prof" \
            --from-env "$tmp" \
            --backup "$backup"
        rm -f "$tmp"
    else
        info "未迁机导入：为本机生成唯一密钥..."
        python3 "$gen" write-prod \
            --template "$example" \
            --output "$env_file" \
            --domain "$NEXUS_DOMAIN" \
            --profile-env "$prof" \
            --fresh \
            --wizard-pending \
            --backup "$backup"
        info "密钥备份: $backup"
        if [[ "$FRESH_INSTALL" != true ]]; then
            info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
        fi
    fi

    chmod 600 "$env_file"
    info "已写入 $env_file"
}

purge_legacy_bundled_services() {
    local root="$1"
    local s
    for s in \
        "$root/deploy/uninstall-mysql-compose.sh" \
        "$root/deploy/uninstall-redis-compose.sh"; do
        if [[ -x "$s" ]]; then
            step "清理遗留 Compose 内置服务: $(basename "$s")"
            NEXUS_ROOT="$root" bash "$s" || true
        fi
    done
}

reset_nexus_state_volume_for_fresh() {
    local vol
    vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
    [[ -n "$vol" ]] || return 0
    warn "全新安装：清除 nexus-state 卷内旧 .env / .install_locked"
    docker run --rm -v "${vol}:/data" alpine:3.19 \
        sh -c 'rm -f /data/.env /data/.install_locked' 2>/dev/null || true
}

restore_install_wizard_html_in_container() {
    local cname="$1"
    [[ -n "$cname" ]] || return 0
    docker exec "$cname" sh -c '
        rm -f /app/.install_locked
        if [ -f /app/web/app/install.html.bak ] && [ ! -f /app/web/app/install.html ]; then
            mv /app/web/app/install.html.bak /app/web/app/install.html
        fi
    ' 2>/dev/null || true
}

compose_up() {
    local root="$1" cname
    save_host_profile "$root"
    purge_legacy_bundled_services "$root"
    sync_1panel_service_hosts "$root"
    if [[ "$FRESH_INSTALL" == true ]]; then
        reset_nexus_state_volume_for_fresh
    fi
    if has_1panel_network; then
        info "已接入 1panel-network（MySQL/Redis 使用 1Panel 容器名互联）"
    fi
    info "档位: ${NEXUS_PROFILE}（仅 Nexus 容器；MySQL/Redis 请自行安装）"
    info "构建并启动（首次约 10～20 分钟）..."
    compose_cmd "$root" up -d --build --remove-orphans
    verify_nexus_1panel_network "$root" || return 1
    if [[ "$FRESH_INSTALL" == true ]]; then
        cname="$(nexus_container_name)"
        restore_install_wizard_html_in_container "$cname"
    fi
}

archive_install_wizard_in_container() {
    local cname="$1"
    [[ -n "$cname" ]] || return 0
    docker exec "$cname" sh -c '
        if [ -f /app/web/app/install.html.bak ]; then
            rm -f /app/web/app/install.html
        elif [ -f /app/web/app/install.html ]; then
            mv /app/web/app/install.html /app/web/app/install.html.bak
        fi
    ' 2>/dev/null || true
}

verify_install_wizard() {
    local root="$1"
    local port="${2:-$(nexus_publish_port "$root")}"
    local code cname
    cname="$(nexus_container_name)"
    if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
        archive_install_wizard_in_container "$cname"
        code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
        if [[ "$code" == "404" ]]; then
            info "  /app/install.html 已归档 → HTTP 404"
            return 0
        fi
        warn "  已锁定但 /app/install.html → HTTP $code"
        return 1
    fi
    code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
    if [[ "$code" == "200" ]]; then
        info "  /app/install.html → HTTP 200"
        return 0
    fi
    warn "  /app/install.html → HTTP $code（尝试同步静态文件…）"
    if [[ -x "$root/deploy/sync-install-wizard-to-container.sh" ]]; then
        NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$root" bash "$root/deploy/sync-install-wizard-to-container.sh" || true
        code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
        if [[ "$code" == "200" ]]; then
            info "  /app/install.html → HTTP 200（已同步）"
            return 0
        fi
    fi
    warn "  请重建镜像: cd $root && docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod build --no-cache nexus"
    return 1
}

verify_health() {
    local root="$1" title="${2:-安装}"
    local i health spa port
    port="$(nexus_publish_port "$root")"
    info "等待服务就绪 (端口 ${port})..."
    for i in $(seq 1 60); do
        health=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || true)
        if [[ "$health" == "ok" ]]; then
            spa=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo "000")
            verify_install_wizard "$root" "$port" || true
            echo ""
            info "═══ ${title}完成 ═══"
            info "  档位: ${NEXUS_PROFILE}"
            info "  /health → ok"
            info "  /app/   → HTTP $spa"
            info "  目录: $root"
            if [[ -d "$root/.git" ]]; then
                info "  版本: $(git -C "$root" log -1 --oneline)"
            fi
            echo ""
            info "1Panel → 网站 → 反代 http://127.0.0.1:${port}  域名: ${NEXUS_DOMAIN}"
            info "配置示例: $root/deploy/1panel/openresty-nexus.conf.example"
            if [[ "$FRESH_INSTALL" == true ]] || ! grep -qE '^NEXUS_SECRET_KEY=.+$' "$root/$ENV_PROD" 2>/dev/null; then
                info "浏览器: https://${NEXUS_DOMAIN}/app/install.html"
            else
                info "浏览器: https://${NEXUS_DOMAIN}/app/"
            fi
            echo ""
            NEXUS_ROOT="$root" bash "$root/deploy/install-nx-cli.sh" 2>/dev/null || true
            info "运维菜单: nx   或   nexus-update"
            check_ports_post "$root"
            return 0
        fi
        sleep 5
    done
    error "健康检查超时"
    check_ports_post "$root" || true
    compose_cmd "$root" logs --tail=80 nexus || true
    return 1
}

ensure_repo() {
    if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
        error "未找到 $NEXUS_ROOT，请先: bash $0 install"
        exit 1
    fi
    if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
        error "缺少 $NEXUS_ROOT/$ENV_PROD"
        exit 1
    fi
}

git_fetch() {
    local url
    url="$(git_remote_url)"
    git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
    git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
}

commits_behind() {
    git -C "$NEXUS_ROOT" rev-list "HEAD..origin/${GIT_BRANCH}" --count 2>/dev/null || echo "0"
}

show_pending() {
    local behind
    behind="$(commits_behind)"
    info "当前: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
    info "远程: $(git -C "$NEXUS_ROOT" log -1 --oneline "origin/${GIT_BRANCH}")"
    if [[ "$behind" == "0" ]]; then
        info "已是最新。"
        return 1
    fi
    warn "落后 ${behind} 个提交:"
    git -C "$NEXUS_ROOT" log --oneline "HEAD..origin/${GIT_BRANCH}"
    return 0
}

backup_mysql() {
    local root="$1" ts file dump_sh
    if [[ "$NO_BACKUP" == true ]]; then
        warn "已跳过备份 (--no-backup)"
        return 0
    fi
    dump_sh="${root}/deploy/mysql_dump_to_file.sh"
    if [[ ! -x "$dump_sh" ]]; then
        warn "缺少 $dump_sh，跳过 MySQL 备份"
        return 0
    fi
    mkdir -p "$NEXUS_BACKUP_DIR"
    ts="$(date +%Y%m%d-%H%M%S)"
    file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
    step "备份 MySQL → $file"
    if ! NEXUS_ROOT="$root" bash "$dump_sh" --output "$file" --root "$root"; then
        if [[ "$REQUIRE_BACKUP" == true ]]; then
            error "mysqldump 失败，已中止（--require-backup）"
            rm -f "$file"
            exit 1
        fi
        warn "mysqldump 失败，继续升级（加 --require-backup 可在备份失败时中止）"
        rm -f "$file"
        return 0
    fi
    gzip -f "$file"
    info "备份: ${file}.gz"
}

git_upgrade() {
    local before after
    before="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
    step "同步 origin/${GIT_BRANCH}"
    git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
    after="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
    info "代码: ${before:0:7} → ${after:0:7}"
}

cmd_init_token() {
    require_root
    local tok="${1:-}"
    if [[ -z "$tok" ]]; then
        error "用法: $0 init-token <Gitea令牌>"
        exit 1
    fi
    printf 'NEXUS_GITEA_TOKEN=%s\n' "$tok" >"$NEXUS_DEPLOY_ENV"
    chmod 600 "$NEXUS_DEPLOY_ENV"
    info "已写入 $NEXUS_DEPLOY_ENV"
}

cmd_install() {
    echo ""
    echo "=========================================="
    echo "  Nexus 1Panel 一键安装"
    echo "  域名: ${NEXUS_DOMAIN}  档位: ${NEXUS_PROFILE}"
    echo "=========================================="
    echo ""

    require_root
    require_token
    resolve_profile
    ensure_packages
    ensure_docker
    check_ports_preflight "$NEXUS_ROOT"
    clone_or_pull
    NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
    write_env_prod "$NEXUS_ROOT"
    compose_up "$NEXUS_ROOT"
    verify_health "$NEXUS_ROOT" "安装" || exit 1
}

cmd_upgrade() {
    echo ""
    echo "=========================================="
    echo "  Nexus 1Panel 一键升级"
    echo "=========================================="
    echo ""

    require_root
    require_token
    ensure_docker
    ensure_repo
    load_saved_profile "$NEXUS_ROOT"
    resolve_profile
    check_ports_preflight "$NEXUS_ROOT"
    git_fetch
    show_pending || true

    if [[ "$CHECK_ONLY" == true ]]; then
        exit 0
    fi
    if [[ "$DRY_RUN" == true ]]; then
        info "dry-run 结束。"
        exit 0
    fi

    local behind
    behind="$(commits_behind)"
    if [[ "$behind" != "0" ]]; then
        backup_mysql "$NEXUS_ROOT"
        git_upgrade
    else
        info "Git 已最新，仍重建 Docker 镜像与容器（避免旧 entrypoint 缓存）"
        if [[ "$NO_BACKUP" != true ]]; then
            backup_mysql "$NEXUS_ROOT"
        fi
    fi

    apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
    save_host_profile "$NEXUS_ROOT"
    purge_legacy_bundled_services "$NEXUS_ROOT"
    sync_1panel_service_hosts "$NEXUS_ROOT"
    sync_env_prod_to_volume "$NEXUS_ROOT"
    tag_rollback_image "$NEXUS_ROOT"
    if [[ "$NO_CACHE" == true ]]; then
        step "无缓存重建 Nexus 镜像..."
        compose_cmd "$NEXUS_ROOT" build --no-cache nexus
    else
        step "重建 Nexus 镜像..."
        compose_cmd "$NEXUS_ROOT" build nexus
    fi
    if ! gate_new_image_import "$NEXUS_ROOT"; then
        exit 1
    fi
    step "重建容器（--remove-orphans 清理旧 mysql/redis）..."
    compose_cmd "$NEXUS_ROOT" up -d --no-build --remove-orphans
    if ! verify_nexus_1panel_network "$NEXUS_ROOT"; then
        rollback_compose_upgrade "$NEXUS_ROOT" || true
        exit 1
    fi
    local port wiz_code i health
    port="$(nexus_publish_port "$NEXUS_ROOT")"
    for i in $(seq 1 30); do
        health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
        [[ "$health" == "ok" ]] && break
        sleep 2
    done
    cname="$(nexus_container_name)"
    if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
        archive_install_wizard_in_container "$cname"
        info "安装已锁定，跳过 install.html 热同步"
    else
        wiz_code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")"
        if [[ "$wiz_code" != "200" && -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then
            NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
        fi
    fi
    set_install_complete "$NEXUS_ROOT"
    if [[ "$PRUNE_IMAGES" == true ]]; then
        docker image prune -f >/dev/null 2>&1 || true
    fi
    NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
    if ! verify_health "$NEXUS_ROOT" "升级"; then
        rollback_compose_upgrade "$NEXUS_ROOT" || true
        exit 1
    fi
}

cmd_check() {
    require_root
    require_token
    ensure_repo
    git_fetch
    show_pending || true
}

parse_common_install() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help) usage ;;
            --skip-clone) SKIP_CLONE=true; shift ;;
            --fresh) FRESH_INSTALL=true; shift ;;
            --reuse-secrets) REUSE_SECRETS=true; shift ;;
            --profile) NEXUS_PROFILE="$2"; shift 2 ;;
            --cpus) CUSTOM_CPUS="$2"; shift 2 ;;
            --mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
            --domain) NEXUS_DOMAIN="$2"; shift 2 ;;
            --from-env) FROM_ENV="$2"; shift 2 ;;
            --root) NEXUS_ROOT="$2"; shift 2 ;;
            --no-backup) NO_BACKUP=true; shift ;;
            --prune|--prune-images) PRUNE_IMAGES=true; shift ;;
            --dry-run) DRY_RUN=true; shift ;;
            *)
                error "未知参数: $1"
                usage
                ;;
        esac
    done
}

parse_common_upgrade() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help) usage ;;
            --profile) NEXUS_PROFILE="$2"; shift 2 ;;
            --cpus) CUSTOM_CPUS="$2"; shift 2 ;;
            --mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
            --root) NEXUS_ROOT="$2"; shift 2 ;;
            --branch) GIT_BRANCH="$2"; shift 2 ;;
            --no-backup) NO_BACKUP=true; shift ;;
            --require-backup) REQUIRE_BACKUP=true; shift ;;
            --prune|--prune-images) PRUNE_IMAGES=true; shift ;;
            --no-cache|--rebuild) NO_CACHE=true; shift ;;
            --dry-run) DRY_RUN=true; shift ;;
            --check) CHECK_ONLY=true; shift ;;
            *)
                error "未知参数: $1"
                usage
                ;;
        esac
    done
}

main() {
    local cmd="${1:-install}"
    shift || true

    load_secrets

    case "$cmd" in
        install)
            parse_common_install "$@"
            cmd_install
            ;;
        upgrade)
            parse_common_upgrade "$@"
            cmd_upgrade
            ;;
        check)
            CHECK_ONLY=true
            parse_common_upgrade "$@"
            cmd_check
            ;;
        init-token)
            cmd_init_token "${1:-}"
            ;;
        ports)
            cmd_ports
            ;;
        -h|--help|help)
            usage
            ;;
        *)
            error "未知子命令: $cmd"
            usage
            ;;
    esac
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi
