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

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# Load library modules
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/docker.sh"
source "$SCRIPT_DIR/lib/config.sh"
source "$SCRIPT_DIR/lib/backup.sh"
source "$SCRIPT_DIR/lib/restore.sh"
source "$SCRIPT_DIR/lib/upgrade.sh"
source "$SCRIPT_DIR/lib/rollback.sh"

VERSION_FILE="$BASE_DIR/VERSION"
ENV_FILE="$BASE_DIR/.env"
COMPOSE_FILE="$BASE_DIR/docker-compose.yml"
CONFIG_DIR="$BASE_DIR/config"
DATA_DIR="$BASE_DIR/data"
BACKUP_DIR="$BASE_DIR/backups"
LOG_DIR="$BASE_DIR/logs"
LOG_FILE="$LOG_DIR/sciomnictl.log"

ensure_dirs() {
    mkdir -p "$DATA_DIR" "$BACKUP_DIR" "$LOG_DIR" "$CONFIG_DIR"
}

get_version() {
    if [ -f "$VERSION_FILE" ]; then
        head -1 "$VERSION_FILE"
    else
        echo "unknown"
    fi
}

cmd_install() {
    local install_mode="offline"
    local target_version=""
    local platform_image_base="crpi-wtb4hzzqrt1pr3rb.cn-hangzhou.personal.cr.aliyuncs.com/insvast/sciomni"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --mode)
                [ $# -ge 2 ] || { log_error "--mode requires a value"; return 1; }
                install_mode="$2"; shift 2
                ;;
            --online) install_mode="online"; shift ;;
            --offline) install_mode="offline"; shift ;;
            --version)
                [ $# -ge 2 ] || { log_error "--version requires a value"; return 1; }
                target_version="$2"; shift 2
                ;;
            --platform-image-base)
                [ $# -ge 2 ] || { log_error "--platform-image-base requires a value"; return 1; }
                platform_image_base="$2"; shift 2
                ;;
            --help|-h)
                cat <<EOF
Usage: sciomnictl install [--mode offline|online] [options]

Options:
  --mode MODE                 Installation mode (default: offline)
  --online                    Shorthand for --mode online
  --offline                   Shorthand for --mode offline
  --version VER               Platform image version required by online mode
  --platform-image-base IMAGE Platform repository without tag
                               (default: $platform_image_base)
EOF
                return 0
                ;;
            *) log_error "Unknown install option: $1"; return 1 ;;
        esac
    done

    case "$install_mode" in
        offline)
            if [ -n "$target_version" ]; then
                log_error "--version is only valid for online installation"
                return 1
            fi
            ;;
        online)
            if [ -z "$target_version" ]; then
                log_error "Online installation requires --version VER"
                return 1
            fi
            if [[ ! "$target_version" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
                log_error "Invalid platform image version: $target_version"
                return 1
            fi
            if [[ "$platform_image_base" != */* ]] || \
               [[ "$platform_image_base" =~ [[:space:]@] ]] || \
               [[ "${platform_image_base##*/}" == *:* ]]; then
                log_error "Invalid platform image base: $platform_image_base"
                return 1
            fi
            ;;
        *) log_error "Invalid installation mode: $install_mode"; return 1 ;;
    esac

    ensure_dirs
    log_info "Starting SciOmni installation (mode: $install_mode, package: v$(get_version))..."

    check_docker_requirements

    if [ ! -f "$ENV_FILE" ]; then
        if [ -f "$BASE_DIR/.env.example" ]; then
            cp "$BASE_DIR/.env.example" "$ENV_FILE"
            log_info "Created .env from template. Please edit it with your settings."
            log_info "  File: $ENV_FILE"
            exit 1
        fi
    fi

    if [ ! -f "$CONFIG_DIR/platform-config.json" ]; then
        if [ -f "$CONFIG_DIR/platform-config.example.json" ]; then
            cp "$CONFIG_DIR/platform-config.example.json" "$CONFIG_DIR/platform-config.json"
            log_info "Created platform-config.json from template."
        fi
    fi

    # Auto-generate secrets if empty or placeholder (must run before sync)
    auto_generate_secrets

    # Sync .env values to platform-config.json (DB, sandbox)
    sync_env_to_config

    # Generate install instance ID for license binding (P2)
    if [ ! -f "$DATA_DIR/install-instance-id" ]; then
        if command -v uuidgen &>/dev/null; then
            uuidgen | tr '[:upper:]' '[:lower:]' > "$DATA_DIR/install-instance-id"
        else
            cat /proc/sys/kernel/random/uuid > "$DATA_DIR/install-instance-id" 2>/dev/null || \
                python3 -c "import uuid; print(uuid.uuid4())" > "$DATA_DIR/install-instance-id"
        fi
        chmod 600 "$DATA_DIR/install-instance-id"
        log_info "Generated install instance ID: $(cat "$DATA_DIR/install-instance-id")"
    fi

    # Verify host sysfs paths are accessible (required for license fingerprint)
    if [ ! -f /etc/machine-id ]; then
        log_warn "/etc/machine-id not found, license fingerprint may be unstable"
    fi
    if [ ! -f /sys/class/dmi/id/product_uuid ]; then
        log_warn "/sys/class/dmi/id/product_uuid not found (VM or permission issue)"
    fi

    # Generate initial admin password
    local admin_password=""
    local creds_file="$DATA_DIR/.initial-credentials"
    if [ ! -f "$creds_file" ]; then
        admin_password=$(generate_admin_password)
    fi

    load_images_if_present

    if [ "$install_mode" = "online" ]; then
        pull_platform_image "$target_version" "$platform_image_base" || return 1
        pull_sandbox_image "$target_version" || return 1
        set_env_value PLATFORM_IMAGE_BASE "$platform_image_base"
        set_env_value PLATFORM_VERSION "$target_version"
        set_env_value SANDBOX_VERSION "$target_version"
    fi

    # NIS mode: patch config for host networking
    if env_val COMPOSE_FILE "" | grep -q "nis"; then
        log_info "NIS mode detected: patching host-network settings"
        local config_json="$CONFIG_DIR/platform-config.json"
        if [ -f "$config_json" ] && command -v jq &>/dev/null; then
            local tmp_json="/tmp/platform-config-nis.$$"
            local nis_http_port nis_grpc_port nis_sandbox_port
            nis_http_port=$(env_val PLATFORM_HTTP_PORT 18080)
            nis_grpc_port=$(env_val PLATFORM_GRPC_PORT 19443)
            nis_sandbox_port=$(env_val SANDBOX_PORT 9090)
            jq --arg http_port "$nis_http_port" \
               --arg grpc_port "$nis_grpc_port" \
               --arg sandbox_port "$nis_sandbox_port" \
               '.database.host = "localhost"
                | .sandbox.endpoint = ("http://localhost:" + $sandbox_port)
                | .server.port = $http_port
                | .server.worker_grpc_addr = ("0.0.0.0:" + $grpc_port)
                | .mcp_gateway.internal_base_url = ("http://localhost:" + $http_port)
                | .mcp_client.trusted_private_origins = (
                    ((.mcp_client.trusted_private_origins // [])
                     + ["http://localhost:" + $http_port,
                        "http://127.0.0.1:" + $http_port])
                    | unique
                  )' \
                "$config_json" > "$tmp_json" 2>/dev/null && mv "$tmp_json" "$config_json"
            log_ok "Database, Sandbox, server and MCP gateway set for host networking"
        fi

        # Some Platform binaries are built with CGO disabled and cannot use
        # NSS directly. Publish resolved passwd/group/shadow snapshots for the
        # NIS overlay. This is only used as a compatibility snapshot.
        prepare_nis_snapshots
    fi

    log_info "Starting services..."
    compose_up

    log_info "Waiting for services to be healthy..."
    wait_for_healthy

    # Create admin user and set initial password via API
    if [ -n "$admin_password" ]; then
        local platform_port
        platform_port=$(env_val PLATFORM_HTTP_PORT 18080)
        local admin_token
        admin_token=$(jq -r '.auth.admin_secret_token // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null)

        # Wait for Platform API to be ready (P5)
        local api_ready=false
        for i in $(seq 1 30); do
            if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${platform_port}/health" 2>/dev/null | grep -q "200"; then
                api_ready=true
                break
            fi
            sleep 1
        done
        if [ "$api_ready" = "false" ]; then
            log_warn "Platform API 未在 30 秒内就绪，跳过管理员初始化"
        fi

        if [ "$api_ready" = "true" ]; then
            local admin_init_ok=true
            # Create admin user (ignore 409 if already exists)
            local create_resp
            create_resp=$(curl -s -o /dev/null -w "%{http_code}" -X POST "http://localhost:${platform_port}/api/v1/users" \
                -H "X-Admin-Token: $admin_token" \
                -H "Content-Type: application/json" \
                -d "$(jq -n --arg p "$admin_password" '{name: "admin", email: "admin@sciomni.local", password: $p}')" 2>/dev/null)
            if [ "$create_resp" != "200" ] && [ "$create_resp" != "201" ] && [ "$create_resp" != "409" ]; then
                admin_init_ok=false
            fi

            # Reset password in case user already existed with different password
            local reset_resp
            reset_resp=$(curl -s -o /dev/null -w "%{http_code}" -X POST "http://localhost:${platform_port}/api/v1/users/admin/reset-password" \
                -H "X-Admin-Token: $admin_token" \
                -H "Content-Type: application/json" \
                -d "$(jq -n --arg p "$admin_password" '{password: $p}')" 2>/dev/null)
            if [ "$reset_resp" != "200" ]; then
                admin_init_ok=false
            fi

            if [ "$admin_init_ok" = "false" ]; then
                log_warn "安装完成，但初始管理员密码设置失败。请检查 Platform 日志并手动重置密码。"
                admin_password=""
            else
                echo "$admin_password" > "$creds_file"
                chmod 600 "$creds_file"
            fi
        fi
    fi

    cp "$CONFIG_DIR/platform-config.json" "$CONFIG_DIR/.last-applied.json"

    # Initialize RAGFlow tenant if knowledge base is enabled
    if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^sciomni-ragflow$"; then
        log_info "Initializing RAGFlow knowledge base tenant..."
        local ragflow_mysql_pw
        ragflow_mysql_pw=$(env_val RAGFLOW_MYSQL_PASSWORD ragflow123)
        # Wait for RAGFlow MySQL
        for i in $(seq 1 30); do
            if docker exec sciomni-ragflow-mysql mysql -uroot -p"$ragflow_mysql_pw" -e "SELECT 1" &>/dev/null; then
                break
            fi
            sleep 2
        done
        # Create tenant, user, and API key
        docker exec sciomni-ragflow-mysql mysql -uroot -p"$ragflow_mysql_pw" rag_flow -e "
            INSERT IGNORE INTO user (id, access_token, nickname, password, email, status, is_superuser, is_authenticated, is_active, is_anonymous, create_time, update_time, create_date, update_date)
            VALUES ('sciomnidefaulttenant00000000000', REPLACE(UUID(),'-',''), 'sciomni-default', '', 'default@sciomni.local', '1', 0, '1', '1', '0', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, NOW(), NOW());
            INSERT IGNORE INTO tenant (id, name, llm_id, embd_id, asr_id, img2txt_id, rerank_id, parser_ids, credit, create_time, update_time, create_date, update_date)
            VALUES ('sciomnidefaulttenant00000000000', 'SciOmni Default', '', 'BAAI/bge-m3', '', '', '', 'naive:General', 100000, UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, NOW(), NOW());
            INSERT IGNORE INTO user_tenant (id, user_id, tenant_id, invited_by, role, create_time, update_time, create_date, update_date)
            VALUES (REPLACE(UUID(),'-',''), 'sciomnidefaulttenant00000000000', 'sciomnidefaulttenant00000000000', 'sciomnidefaulttenant00000000000', 'owner', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, NOW(), NOW());
            INSERT IGNORE INTO api_token (tenant_id, token, create_time, update_time, create_date, update_date)
            VALUES ('sciomnidefaulttenant00000000000', 'ragflow-sciomni-api-key', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, NOW(), NOW());
            INSERT IGNORE INTO tenant_llm (tenant_id, llm_factory, model_type, llm_name, api_key, max_tokens, used_tokens, create_time, create_date, update_time, update_date)
            VALUES ('sciomnidefaulttenant00000000000', 'BAAI', 'embedding', 'BAAI/bge-m3', '', 0, 0, UNIX_TIMESTAMP()*1000, NOW(), UNIX_TIMESTAMP()*1000, NOW());
        " 2>/dev/null && log_ok "RAGFlow tenant initialized" || log_warn "RAGFlow tenant initialization failed (may already exist)"
    fi

    if [ "$install_mode" = "online" ]; then
        printf '%s\n' "$target_version" > "$VERSION_FILE"
    fi

    log_ok "Installation complete!"
    log_info "  Platform: http://localhost:$(env_val PLATFORM_HTTP_PORT 18080)"
    log_info "  Version:  $(get_version)"

    if [ -n "$admin_password" ]; then
        echo ""
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        log_ok "初始管理员账号:"
        log_info "  用户名: admin"
        log_info "  密码:   $admin_password"
        echo ""
        log_warn "请立即登录并修改初始密码！"
        log_info "  密码已保存至: $creds_file"
        log_info "  修改密码后建议删除该文件: rm $creds_file"
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    fi

    # NIS/LDAP mode guidance
    local ext_auth_enabled
    ext_auth_enabled=$(jq -r '.external_auth.enabled // false' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
    if [ "$ext_auth_enabled" = "true" ]; then
        local ext_auth_provider
        ext_auth_provider=$(jq -r '.external_auth.provider // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
        echo ""
        log_info "外部认证已启用: $ext_auth_provider"
        if [ "$ext_auth_provider" = "nis" ]; then
            log_info "NIS 模式需要使用 NIS 镜像并启用 compose overlay:"
            log_info "  1. 编辑 .env: PLATFORM_IMAGE_VARIANT=-nis"
            log_info "  2. 取消注释 .env: COMPOSE_FILE=docker-compose.yml:docker-compose.nis.yml"
            log_info "  3. 配置 NIS: config/nis/defaultdomain 和 config/nis/yp.conf"
            log_info "  4. 重启: docker compose -f docker-compose.yml -f docker-compose.nis.yml up -d platform"
        elif [ "$ext_auth_provider" = "ldap" ]; then
            log_info "LDAP 模式使用标准 Alpine 镜像即可"
        fi
        log_info "启用后请在管理界面为用户设置 linux_username（linux_user 模式必需）"
    fi

    log_action "install" "mode=$install_mode version=$(get_version)"
}

cmd_uninstall() {
    local keep_data=false
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --keep-data) keep_data=true; shift ;;
            *) shift ;;
        esac
    done

    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    log_warn "卸载将执行以下操作:"
    log_info "  1. 停止所有 SciOmni 服务容器"
    if [ "$keep_data" = "true" ]; then
        log_info "  2. 删除容器（保留数据卷）"
    else
        log_info "  2. 删除容器及所有数据卷（数据库、证书、sandbox 数据）"
    fi
    log_info "  3. 清理运行时文件（日志、锁文件）"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""

    if [ "$keep_data" = "false" ]; then
        log_warn "此操作不可逆！所有数据（数据库、配置绑定、许可证状态）将被永久删除。"
    fi

    if ! confirm "确认卸载 SciOmni?"; then
        log_info "已取消"
        return 0
    fi

    log_info "Stopping and removing services..."
    if [ "$keep_data" = "true" ]; then
        compose_cmd down
    else
        compose_cmd down -v
    fi

    # Clean runtime files
    rm -f "$LOG_DIR"/*.log "$DATA_DIR/.sciomnictl.lock" 2>/dev/null || true

    log_ok "SciOmni 已卸载"
    if [ "$keep_data" = "true" ]; then
        log_info "数据卷已保留，重新安装后可恢复数据"
        log_info "如需彻底清理数据卷: docker volume rm \$(docker volume ls -q --filter name=sciomni)"
    else
        log_info "所有数据已清除"
    fi
    log_info "安装目录 ($BASE_DIR) 未删除，如需移除请手动执行: rm -rf $BASE_DIR"
    log_action "uninstall" "keep_data=$keep_data"
}

cmd_start() {
    ensure_dirs
    prepare_nis_snapshots
    log_info "Starting SciOmni services..."
    compose_up
    if ! wait_for_healthy; then
        for container in sciomni-postgres sciomni-sandbox sciomni-platform; do
            detect_crash_loop "$container"
        done
        log_error "Services failed to become healthy"
        return 1
    fi
    log_info "All services started."
    log_action "start"
}

cmd_stop() {
    log_info "Stopping SciOmni services..."
    compose_down
    log_info "All services stopped."
    log_action "stop"
}

prepare_nis_snapshots() {
    if ! env_val COMPOSE_FILE "" | grep -q "nis"; then
        return 0
    fi

    mkdir -p "$CONFIG_DIR/nis"

    # If Docker previously created bind-mount source paths as directories,
    # remove them. These paths must be regular files for the NIS overlay.
    local snapshot
    for snapshot in passwd group shadow; do
        if [ -d "$CONFIG_DIR/nis/$snapshot" ] && [ ! -f "$CONFIG_DIR/nis/$snapshot" ]; then
            log_warn "Removing invalid NIS snapshot directory: config/nis/$snapshot"
            rm -rf "$CONFIG_DIR/nis/$snapshot"
        fi
    done

    if ! command -v getent &>/dev/null; then
        log_error "NIS mode requires getent to generate passwd/group/shadow snapshots"
        return 1
    fi

    getent passwd > "$CONFIG_DIR/nis/passwd" 2>/dev/null || {
        log_error "Failed to generate config/nis/passwd"
        return 1
    }
    getent group > "$CONFIG_DIR/nis/group" 2>/dev/null || {
        log_error "Failed to generate config/nis/group"
        return 1
    }
    getent shadow > "$CONFIG_DIR/nis/shadow" 2>/dev/null || {
        log_error "Failed to generate config/nis/shadow (run as root or with shadow access)"
        return 1
    }

    chmod 644 "$CONFIG_DIR/nis/passwd" "$CONFIG_DIR/nis/group"
    chmod 600 "$CONFIG_DIR/nis/shadow"
    log_ok "NIS passwd/group/shadow snapshots generated"
}

cmd_restart() {
    local service="${1:-}"
    if [ -n "$service" ]; then
        log_info "Restarting $service..."
        compose_restart "$service"
    else
        log_info "Restarting all services..."
        compose_down
        compose_up
        wait_for_healthy
    fi
    log_action "restart" "service=${service:-all}"
}

cmd_status() {
    local version
    version=$(get_version)

    local platform_commit sandbox_commit
    platform_commit=$(get_container_label "sciomni-platform" "org.opencontainers.image.revision" 2>/dev/null || echo "n/a")
    sandbox_commit=$(get_container_label "sciomni-sandbox" "org.opencontainers.image.revision" 2>/dev/null || echo "n/a")

    echo "SciOmni v${version} (platform: ${platform_commit}, sandbox: ${sandbox_commit})"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

    print_service_status "PostgreSQL" "sciomni-postgres" "$(env_val DB_PORT 5432)"
    print_service_status "Sandbox" "sciomni-sandbox" "$(env_val SANDBOX_PORT 9090)"
    print_service_status "Platform" "sciomni-platform" "$(env_val PLATFORM_HTTP_PORT 18080)"

    # RAGFlow (optional)
    if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^sciomni-ragflow$"; then
        print_service_status "RAGFlow" "sciomni-ragflow" "$(env_val RAGFLOW_PORT 9380)"
    fi

    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

    local platform_port
    platform_port=$(env_val PLATFORM_HTTP_PORT 18080)
    local admin_token
    admin_token=$(jq -r '.auth.admin_secret_token // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null || echo "")

    if [ -n "$admin_token" ]; then
        local license_json
        license_json=$(curl -s -H "X-Admin-Token: $admin_token" "http://localhost:${platform_port}/api/v1/admin/license" 2>/dev/null || echo "")
        if [ -n "$license_json" ] && echo "$license_json" | jq -e '.status' >/dev/null 2>&1; then
            local lic_status
            lic_status=$(echo "$license_json" | jq -r '.status')
            case "$lic_status" in
                Licensed)
                    local days_rem active max_u
                    days_rem=$(echo "$license_json" | jq -r '.days_remaining // "?"')
                    active=$(echo "$license_json" | jq -r '.active_users // 0')
                    max_u=$(echo "$license_json" | jq -r '.max_active_users // 0')
                    echo "License:     有效 (剩余 ${days_rem} 天, ${active}/${max_u} 用户)"
                    ;;
                GracePeriod)
                    local days_rem
                    days_rem=$(echo "$license_json" | jq -r '.days_remaining // "?"')
                    echo "License:     宽限期 (剩余 ${days_rem} 天)"
                    ;;
                Unlicensed)
                    echo "License:     未激活"
                    ;;
                *)
                    echo "License:     $lic_status"
                    ;;
            esac
        fi

        local workers_json
        workers_json=$(curl -s -H "X-Admin-Token: $admin_token" "http://localhost:${platform_port}/api/v1/admin/workers" 2>/dev/null || echo "")
        if [ -n "$workers_json" ] && echo "$workers_json" | jq -e '.data.workers' >/dev/null 2>&1; then
            echo "Workers:"
            echo "$workers_json" | jq -r '.data.workers[] | "  \(.id)\t\(.version // "n/a")\t\(.status)\t\(.connected_at // "n/a")\t\(.capabilities // [] | join(", "))"' 2>/dev/null || echo "  (unable to parse worker list)"
        fi
    fi
}

cmd_logs() {
    local service="${1:-}"
    local lines="${2:-50}"
    if [ -n "$service" ]; then
        docker logs --tail "$lines" -f "sciomni-${service}"
    else
        docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs --tail "$lines" -f
    fi
}

cmd_config() {
    local subcmd="${1:-}"
    shift || true
    case "$subcmd" in
        validate)
            config_validate "$CONFIG_DIR/platform-config.json"
            ;;
        apply)
            config_apply "$CONFIG_DIR/platform-config.json" "$CONFIG_DIR/.last-applied.json" "$@"
            log_action "config-apply"
            ;;
        *)
            echo "Usage: sciomnictl config <validate|apply>"
            exit 1
            ;;
    esac
}

cmd_backup() {
    ensure_dirs
    run_backup "$@"
    log_action "backup"
}

cmd_restore() {
    ensure_dirs
    local restore_file="${1:-}"
    run_restore "$@"
    local rc=$?
    if [ $rc -eq 0 ]; then
        log_action "restore" "file=$restore_file"
    elif [ $rc -eq 2 ]; then
        log_action "restore-partial" "file=$restore_file"
    fi
    return $rc
}

cmd_upgrade() {
    ensure_dirs
    run_upgrade "$@"
    log_action "upgrade"
}

cmd_patch() {
    ensure_dirs
    run_patch "$@"
    log_action "patch"
}

cmd_rollback() {
    ensure_dirs
    run_rollback "$@"
    log_action "rollback"
}

cmd_drain_workers() {
    local timeout="7200"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --timeout) timeout="$2"; shift 2 ;;
            *) shift ;;
        esac
    done
    drain_workers "$timeout"
    log_action "drain-workers" "timeout=$timeout"
}

cmd_version() {
    local version
    version=$(get_version)
    echo "SciOmni v${version}"

    local platform_commit sandbox_commit
    platform_commit=$(get_container_label "sciomni-platform" "org.opencontainers.image.revision" 2>/dev/null || echo "n/a")
    sandbox_commit=$(get_container_label "sciomni-sandbox" "org.opencontainers.image.revision" 2>/dev/null || echo "n/a")

    local pv sv
    pv=$(env_val PLATFORM_VERSION "n/a")
    sv=$(env_val SANDBOX_VERSION "n/a")
    echo "  Platform:  ${pv} (commit: ${platform_commit})"
    echo "  Sandbox:   ${sv} (commit: ${sandbox_commit})"
}

cmd_preflight() {
    log_info "Running pre-installation checks..."
    local issues=0
    local warnings=0

    echo ""
    echo "[ 环境检查 ]"

    # Docker
    if ! command -v docker &>/dev/null; then
        log_error "Docker 未安装"
        issues=$((issues + 1))
    else
        local docker_ver
        docker_ver=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1 || true)
        local docker_major
        docker_major=$(echo "$docker_ver" | cut -d. -f1)
        if [ "${docker_major:-0}" -lt 24 ]; then
            log_error "Docker 版本过低: $docker_ver (需要 24.0+)"
            issues=$((issues + 1))
        else
            log_ok "Docker: $(docker --version 2>/dev/null)"
        fi
    fi

    # Docker Compose V2
    if ! docker compose version &>/dev/null 2>&1; then
        log_error "Docker Compose V2 未安装 (需要 'docker compose' 子命令)"
        issues=$((issues + 1))
    else
        log_ok "Docker Compose: $(docker compose version --short 2>/dev/null)"
    fi

    # Docker daemon
    if ! docker info &>/dev/null 2>&1; then
        log_error "Docker daemon 未运行"
        issues=$((issues + 1))
    else
        log_ok "Docker daemon: 运行中"
    fi

    # jq
    if ! command -v jq &>/dev/null; then
        log_error "jq 未安装 (JSON 处理工具)"
        issues=$((issues + 1))
    else
        log_ok "jq: $(jq --version 2>/dev/null)"
    fi

    # Disk space
    local disk_free_kb
    disk_free_kb=$(df -k "$BASE_DIR" | awk 'NR==2{print $4}')
    local disk_free_gb=$((disk_free_kb / 1048576))
    if [ "$disk_free_kb" -lt 10485760 ]; then
        log_error "磁盘空间不足: ${disk_free_gb}GB 可用 (需要至少 10GB)"
        issues=$((issues + 1))
    elif [ "$disk_free_kb" -lt 20971520 ]; then
        log_warn "磁盘空间偏少: ${disk_free_gb}GB 可用 (建议 20GB+)"
        warnings=$((warnings + 1))
    else
        log_ok "磁盘空间: ${disk_free_gb}GB 可用"
    fi

    # Memory
    local mem_total_kb
    if [ -f /proc/meminfo ]; then
        mem_total_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    else
        mem_total_kb=$(sysctl -n hw.memsize 2>/dev/null | awk '{print int($1/1024)}')
    fi
    local mem_total_gb=$((mem_total_kb / 1048576))
    if [ "${mem_total_kb:-0}" -lt 3145728 ]; then
        log_error "内存不足: ${mem_total_gb}GB (需要至少 4GB)"
        issues=$((issues + 1))
    elif [ "${mem_total_kb:-0}" -lt 7340032 ]; then
        log_warn "内存偏少: ${mem_total_gb}GB (建议 8GB+)"
        warnings=$((warnings + 1))
    else
        log_ok "系统内存: ${mem_total_gb}GB"
    fi

    # Port availability
    echo ""
    echo "[ 端口检查 ]"
    local ports_to_check=""
    if [ -f "$ENV_FILE" ]; then
        ports_to_check="$(env_val DB_PORT 5432) $(env_val SANDBOX_PORT 9090) $(env_val PLATFORM_HTTP_PORT 18080) $(env_val PLATFORM_GRPC_PORT 19443)"
    else
        ports_to_check="5432 9090 18080 19443"
    fi
    for port in $ports_to_check; do
        if ss -tlnp 2>/dev/null | grep -q ":${port} " || \
           lsof -iTCP:"$port" -sTCP:LISTEN &>/dev/null; then
            log_warn "端口 $port 已被占用"
            warnings=$((warnings + 1))
        else
            log_ok "端口 $port: 可用"
        fi
    done

    # Configuration files
    echo ""
    echo "[ 配置文件检查 ]"

    if [ -f "$ENV_FILE" ]; then
        log_ok ".env 文件存在"
        # Check for placeholder values
        if grep -q "CHANGE_ME" "$ENV_FILE" 2>/dev/null; then
            log_error ".env 中存在未修改的占位符 (CHANGE_ME)"
            issues=$((issues + 1))
        else
            log_ok ".env 占位符已替换"
        fi
    else
        if [ -f "$BASE_DIR/.env.example" ]; then
            log_warn ".env 文件不存在 (需从 .env.example 创建)"
            warnings=$((warnings + 1))
        else
            log_error ".env 文件不存在且无模板"
            issues=$((issues + 1))
        fi
    fi

    if [ -f "$CONFIG_DIR/platform-config.json" ]; then
        log_ok "platform-config.json 存在"
        # JSON syntax check
        if ! jq empty "$CONFIG_DIR/platform-config.json" 2>/dev/null; then
            log_error "platform-config.json JSON 格式错误"
            issues=$((issues + 1))
        else
            log_ok "platform-config.json 语法正确"
        fi
        # Check required fields
        local autogen_fields=""
        local sync_fields=""
        local admin_token_val
        admin_token_val=$(jq -r '.auth.admin_secret_token // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
        if [ -z "$admin_token_val" ] || [ "$admin_token_val" = "CHANGE_ME_ADMIN_TOKEN" ]; then
            autogen_fields="$autogen_fields admin_secret_token"
        fi
        local jwt_val
        jwt_val=$(jq -r '.auth.jwt_secret // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
        if [ -z "$jwt_val" ] || [ "$jwt_val" = "CHANGE_ME_JWT_SECRET" ]; then
            autogen_fields="$autogen_fields jwt_secret"
        fi
        if [ "$(jq -r '.database.password // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null)" = "" ]; then
            sync_fields="$sync_fields database.password"
        fi
        if [ "$(jq -r '.sandbox.api_key // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null)" = "" ]; then
            sync_fields="$sync_fields sandbox.api_key"
        fi
        if [ -n "$autogen_fields" ]; then
            log_info "以下字段为空或占位符，安装时将自动生成:$autogen_fields"
        fi
        if [ -n "$sync_fields" ]; then
            log_info "以下字段为空，安装时将从 .env 同步:$sync_fields"
        fi
        if [ -z "$autogen_fields" ] && [ -z "$sync_fields" ]; then
            log_ok "platform-config.json 必要字段完整"
        else
            log_ok "platform-config.json 格式正确（部分字段将在安装时自动填充）"
        fi
        # Check .env has the source values for sync
        if [ -f "$ENV_FILE" ]; then
            local env_db_pass env_sandbox_key
            env_db_pass=$(env_val DB_PASSWORD "")
            env_sandbox_key=$(env_val SANDBOX_API_KEY "")
            if [ -n "$sync_fields" ]; then
                if echo "$sync_fields" | grep -q "database.password" && [ -z "$env_db_pass" ]; then
                    log_error ".env 中 DB_PASSWORD 为空，无法同步到 platform-config.json"
                    issues=$((issues + 1))
                fi
                if echo "$sync_fields" | grep -q "sandbox.api_key" && [ -z "$env_sandbox_key" ]; then
                    log_error ".env 中 SANDBOX_API_KEY 为空，无法同步到 platform-config.json"
                    issues=$((issues + 1))
                fi
            fi
        fi
    else
        log_warn "platform-config.json 不存在 (需从模板创建)"
        warnings=$((warnings + 1))
    fi

    local compose_file_list compose_file
    compose_file_list=$(env_val COMPOSE_FILE "$COMPOSE_FILE")
    local -a compose_files=()
    local compose_missing=0
    IFS=':' read -ra compose_files <<< "$compose_file_list"
    for compose_file in "${compose_files[@]}"; do
        [ -n "$compose_file" ] || continue
        if [[ "$compose_file" != /* ]]; then
            compose_file="$BASE_DIR/$compose_file"
        fi
        if [ -f "$compose_file" ]; then
            log_ok "Compose 文件存在: $(basename "$compose_file")"
        else
            log_error "Compose 文件不存在: $compose_file"
            issues=$((issues + 1))
            compose_missing=1
        fi
    done
    if [ "$compose_missing" -eq 0 ] && ! compose_cmd config --quiet; then
        log_error "Compose 配置校验失败"
        issues=$((issues + 1))
    fi

    # External auth (LDAP/AD/NIS) check
    if [ -f "$CONFIG_DIR/platform-config.json" ]; then
        local ext_auth_enabled
        ext_auth_enabled=$(jq -r '.external_auth.enabled // false' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
        if [ "$ext_auth_enabled" = "true" ]; then
            echo ""
            echo "[ 外部认证检查 ]"
            local provider
            provider=$(jq -r '.external_auth.provider // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)

            if [ "$provider" = "ldap" ]; then
                local ldap_url
                ldap_url=$(jq -r '.external_auth.ldap.url // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                if [ -z "$ldap_url" ]; then
                    log_error "LDAP/AD 已启用但 url 未配置"
                    issues=$((issues + 1))
                else
                    local ldap_host ldap_port
                    ldap_host=$(echo "$ldap_url" | sed 's|ldap[s]*://||;s|/.*||;s|:.*||')
                    ldap_port=$(echo "$ldap_url" | sed -n 's|^ldaps\?://[^/:]*:\([0-9]\+\).*$|\1|p')
                    ldap_port="${ldap_port:-389}"
                    if tcp_reachable "$ldap_host" "$ldap_port"; then
                        log_ok "LDAP/AD ($ldap_url): 可达"
                    else
                        log_error "LDAP/AD ($ldap_url): 不可达"
                        issues=$((issues + 1))
                    fi
                    # Validate required LDAP fields
                    local ldap_bind_dn ldap_base_dn ldap_bind_password ldap_user_filter
                    ldap_bind_dn=$(jq -r '.external_auth.ldap.bind_dn // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                    ldap_base_dn=$(jq -r '.external_auth.ldap.base_dn // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                    ldap_bind_password=$(jq -r '.external_auth.ldap.bind_password // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                    ldap_user_filter=$(jq -r '.external_auth.ldap.user_filter // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                    if [ -z "$ldap_bind_dn" ]; then
                        log_error "LDAP/AD bind_dn 未配置"
                        issues=$((issues + 1))
                    fi
                    if [ -z "$ldap_base_dn" ]; then
                        log_error "LDAP/AD base_dn 未配置"
                        issues=$((issues + 1))
                    fi
                    if [ -z "$ldap_bind_password" ]; then
                        log_error "LDAP/AD bind_password 未配置"
                        issues=$((issues + 1))
                    fi
                    if [ -z "$ldap_user_filter" ]; then
                        log_error "LDAP/AD user_filter 未配置"
                        issues=$((issues + 1))
                    fi
                    if [ -n "$ldap_bind_dn" ] && [ -n "$ldap_base_dn" ] && \
                       [ -n "$ldap_bind_password" ] && [ -n "$ldap_user_filter" ]; then
                        log_ok "LDAP/AD 必要字段已配置 (bind_dn, base_dn, bind_password, user_filter)"
                    fi
                    # Check ldapsearch tool availability
                    if command -v ldapsearch &>/dev/null; then
                        log_ok "ldapsearch 工具: 可用"
                    else
                        log_warn "ldapsearch 工具未安装 (无法进行连接验证，但不影响运行)"
                        warnings=$((warnings + 1))
                    fi
                fi
            elif [ "$provider" = "nis" ]; then
                local pam_service
                pam_service=$(jq -r '.external_auth.pam.service_name // "sciomni"' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                local nis_variant
                nis_variant=$(env_val PLATFORM_IMAGE_VARIANT "")
                if [ "$nis_variant" = "-nis" ]; then
                    log_ok "NIS 模式: 使用 NIS Docker 镜像 (容器内运行 NIS 客户端)"
                    log_info "NIS 配置文件: config/nis/ 目录"
                    if [ -f "$BASE_DIR/config/nis/yp.conf" ]; then
                        log_ok "NIS yp.conf: 存在"
                    else
                        log_warn "NIS yp.conf 不存在"
                        warnings=$((warnings + 1))
                    fi
                    if [ -f "$BASE_DIR/config/nis/defaultdomain" ]; then
                        log_ok "NIS defaultdomain: 存在"
                    else
                        log_warn "NIS defaultdomain 不存在"
                        warnings=$((warnings + 1))
                    fi
                else
                    log_warn "NIS 已启用但 PLATFORM_IMAGE_VARIANT 未设为 -nis"
                    log_warn "标准 Alpine 镜像不支持 NIS 认证"
                    log_warn "请在 .env 中设置: PLATFORM_IMAGE_VARIANT=-nis"
                    log_warn "并设置: COMPOSE_FILE=docker-compose.yml:docker-compose.nis.yml"
                    issues=$((issues + 1))
                fi
                # Check PAM configuration file exists on host (for container mount)
                if [ -f "$BASE_DIR/config/nis/pam.d/$pam_service" ]; then
                    log_ok "PAM 配置 ($BASE_DIR/config/nis/pam.d/$pam_service): 存在"
                elif [ -f "/etc/pam.d/$pam_service" ]; then
                    log_ok "PAM 配置 (/etc/pam.d/$pam_service): 存在 (宿主机)"
                else
                    log_warn "PAM 配置不存在 (需要创建 $BASE_DIR/config/nis/pam.d/$pam_service)"
                    warnings=$((warnings + 1))
                fi
            fi
        fi
    fi

    # Images / resources
    echo ""
    echo "[ 安装资源检查 ]"

    local images_dir="$BASE_DIR/images"
    if [ -d "$images_dir" ] && ls "$images_dir"/*.tar.gz &>/dev/null 2>&1; then
        log_ok "离线镜像目录存在"
        local img_count=0
        local img_corrupt=0
        for img in "$images_dir"/*.tar.gz; do
            [ -f "$img" ] || continue
            img_count=$((img_count + 1))
            if ! gzip -t "$img" 2>/dev/null; then
                log_error "镜像文件损坏: $(basename "$img")"
                img_corrupt=$((img_corrupt + 1))
                issues=$((issues + 1))
            fi
        done
        if [ $img_corrupt -eq 0 ]; then
            log_ok "离线镜像完整性检查通过 ($img_count 个文件)"
        fi
        # Check expected images
        local platform_ver sandbox_ver
        if [ -f "$ENV_FILE" ]; then
            platform_ver=$(env_val PLATFORM_VERSION "0.1.0")
            sandbox_ver=$(env_val SANDBOX_VERSION "0.1.0")
        fi
        local has_platform=0 has_sandbox=0 has_postgres=0
        for img in "$images_dir"/*.tar.gz; do
            case "$(basename "$img")" in
                *platform*) has_platform=1 ;;
                *sandbox*) has_sandbox=1 ;;
                *postgres*) has_postgres=1 ;;
            esac
        done
        if [ $has_platform -eq 0 ]; then
            log_warn "缺少 Platform 镜像文件"
            warnings=$((warnings + 1))
        fi
        if [ $has_sandbox -eq 0 ]; then
            log_warn "缺少 Sandbox 镜像文件"
            warnings=$((warnings + 1))
        fi
        if [ $has_postgres -eq 0 ]; then
            log_warn "缺少 PostgreSQL 镜像文件"
            warnings=$((warnings + 1))
        fi
    else
        log_warn "无离线镜像 (images/ 目录为空或不存在)，安装时将在线拉取"
        warnings=$((warnings + 1))
    fi

    # Scripts integrity
    local scripts_ok=1
    for script in "$BASE_DIR/scripts/lib/common.sh" "$BASE_DIR/scripts/lib/docker.sh" \
                  "$BASE_DIR/scripts/lib/config.sh" "$BASE_DIR/scripts/lib/backup.sh" \
                  "$BASE_DIR/scripts/lib/restore.sh" "$BASE_DIR/scripts/lib/upgrade.sh" \
                  "$BASE_DIR/scripts/lib/rollback.sh"; do
        if [ ! -f "$script" ]; then
            log_error "脚本缺失: $(basename "$script")"
            scripts_ok=0
            issues=$((issues + 1))
        fi
    done
    if [ $scripts_ok -eq 1 ]; then
        log_ok "管理脚本完整"
    fi

    # VERSION file
    if [ -f "$VERSION_FILE" ]; then
        log_ok "VERSION 文件: $(cat "$VERSION_FILE")"
    else
        log_warn "VERSION 文件不存在"
        warnings=$((warnings + 1))
    fi

    # Summary
    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    if [ $issues -eq 0 ] && [ $warnings -eq 0 ]; then
        log_ok "预检通过 — 环境满足安装条件"
    elif [ $issues -eq 0 ]; then
        log_warn "预检通过 (有 $warnings 个警告) — 可以安装但建议关注警告项"
    else
        log_error "预检未通过 — $issues 个错误, $warnings 个警告"
        log_error "请修复上述错误后重新执行 preflight 检查"
        return 1
    fi
}

cmd_license() {
    local subcmd="${1:-}"
    shift || true

    local platform_port
    platform_port=$(env_val PLATFORM_HTTP_PORT 18080)
    local admin_token
    admin_token=$(jq -r '.auth.admin_secret_token // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null || echo "")
    local base_url="http://localhost:${platform_port}"

    if [ -z "$admin_token" ]; then
        log_error "无法读取 admin_secret_token，请检查 $CONFIG_DIR/platform-config.json"
        return 1
    fi

    case "$subcmd" in
        activate|update)
            local file="${1:-}"
            if [ -z "$file" ]; then
                log_error "用法: sciomnictl license $subcmd <license-file>"
                return 1
            fi
            if [ ! -f "$file" ]; then
                log_error "许可证文件不存在: $file"
                return 1
            fi
            local resp
            resp=$(curl -s -w "\n%{http_code}" -X PUT \
                -H "X-Admin-Token: $admin_token" \
                -F "file=@$file" \
                "$base_url/api/v1/admin/license")
            local http_code body
            http_code=$(echo "$resp" | tail -1)
            body=$(echo "$resp" | sed '$d')
            if [ "$http_code" = "200" ]; then
                log_ok "许可证${subcmd}成功"
                echo "$body" | jq . 2>/dev/null || echo "$body"
            else
                log_error "许可证${subcmd}失败 (HTTP $http_code)"
                echo "$body" | jq . 2>/dev/null || echo "$body"
                return 1
            fi
            ;;
        deactivate)
            local file="${1:-}"
            local resp
            if [ -n "$file" ] && [ -f "$file" ]; then
                log_info "使用许可证文件进行双因子验证"
                resp=$(curl -s -w "\n%{http_code}" -X POST \
                    -H "X-Admin-Token: $admin_token" \
                    -F "file=@$file" \
                    "$base_url/api/v1/admin/license/deactivate")
            elif [ -f "$DATA_DIR/.offline-secret" ]; then
                log_info "使用离线密钥进行双因子验证"
                local secret
                secret=$(cat "$DATA_DIR/.offline-secret")
                resp=$(curl -s -w "\n%{http_code}" -X POST \
                    -H "X-Admin-Token: $admin_token" \
                    -H "Content-Type: application/json" \
                    -d "{\"offline_secret\": \"$secret\"}" \
                    "$base_url/api/v1/admin/license/deactivate")
            else
                log_error "解绑需要双因子验证。请提供许可证文件或确保 $DATA_DIR/.offline-secret 存在"
                log_info "用法: sciomnictl license deactivate [license-file]"
                log_info "  传入许可证文件路径，或自动使用 data/.offline-secret"
                return 1
            fi
            local http_code body
            http_code=$(echo "$resp" | tail -1)
            body=$(echo "$resp" | sed '$d')
            if [ "$http_code" = "200" ]; then
                log_ok "许可证已解绑"
                echo "$body" | jq . 2>/dev/null || echo "$body"
            else
                log_error "许可证解绑失败 (HTTP $http_code)"
                echo "$body" | jq . 2>/dev/null || echo "$body"
                return 1
            fi
            ;;
        status)
            local resp
            resp=$(curl -s -H "X-Admin-Token: $admin_token" "$base_url/api/v1/admin/license")
            if echo "$resp" | jq -e '.status' >/dev/null 2>&1; then
                local status subject not_after max_users active_users days_remaining
                status=$(echo "$resp" | jq -r '.status')
                case "$status" in
                    Licensed)
                        subject=$(echo "$resp" | jq -r '.subject // "N/A"')
                        not_after=$(echo "$resp" | jq -r '.not_after // "N/A"')
                        max_users=$(echo "$resp" | jq -r '.max_active_users // 0')
                        active_users=$(echo "$resp" | jq -r '.active_users // 0')
                        days_remaining=$(echo "$resp" | jq -r '.days_remaining // 0')
                        echo "SciOmni License"
                        echo "  状态:         正式授权 (Licensed)"
                        echo "  授权对象:     $subject"
                        echo "  有效期至:     $not_after (剩余 ${days_remaining} 天)"
                        echo "  并发用户:     ${active_users}/${max_users}"
                        ;;
                    GracePeriod)
                        days_remaining=$(echo "$resp" | jq -r '.days_remaining // 0')
                        echo "SciOmni License"
                        echo "  状态:         宽限期 (GracePeriod)"
                        echo "  剩余宽限天数: $days_remaining"
                        ;;
                    Unlicensed)
                        local reason
                        reason=$(echo "$resp" | jq -r '.failure_reason // "未知"')
                        echo "SciOmni License"
                        echo "  状态:         未激活 (Unlicensed)"
                        echo "  原因:         $reason"
                        ;;
                    *)
                        echo "SciOmni License"
                        echo "  状态:         $status"
                        echo "$resp" | jq . 2>/dev/null
                        ;;
                esac
            else
                log_error "无法获取许可证状态"
                echo "$resp"
                return 1
            fi
            ;;
        verify)
            local file="${1:-}"
            if [ -z "$file" ]; then
                log_error "用法: sciomnictl license verify <license-file>"
                return 1
            fi
            if [ ! -f "$file" ]; then
                log_error "许可证文件不存在: $file"
                return 1
            fi
            local resp
            resp=$(curl -s -w "\n%{http_code}" -X POST \
                -H "X-Admin-Token: $admin_token" \
                -F "file=@$file" \
                "$base_url/api/v1/admin/license/verify")
            local http_code body
            http_code=$(echo "$resp" | tail -1)
            body=$(echo "$resp" | sed '$d')
            if [ "$http_code" = "200" ]; then
                local can_activate
                can_activate=$(echo "$body" | jq -r '.can_activate // false')
                if [ "$can_activate" = "true" ]; then
                    log_ok "许可证验证通过，可以激活"
                else
                    log_warn "许可证验证通过但无法在当前机器激活"
                fi
                echo "$body" | jq . 2>/dev/null || echo "$body"
            else
                log_error "许可证验证失败 (HTTP $http_code)"
                echo "$body" | jq . 2>/dev/null || echo "$body"
                return 1
            fi
            ;;
        fingerprint|request-info)
            local resp
            resp=$(curl -s -H "X-Admin-Token: $admin_token" "$base_url/api/v1/admin/license/request-info")
            if echo "$resp" | jq -e '.hardware_id' >/dev/null 2>&1; then
                echo "机器指纹信息:"
                echo "  hardware_id:  $(echo "$resp" | jq -r '.hardware_id')"
                echo "  instance_id:  $(echo "$resp" | jq -r '.instance_id // "未找到"')"
                echo "  complete:     $(echo "$resp" | jq -r '.complete // false')"
            else
                log_error "无法获取机器指纹"
                echo "$resp"
                return 1
            fi
            ;;
        *)
            cat <<LICEOF
用法: sciomnictl license <subcommand> [options]

子命令:
  activate <file>     激活许可证（首次激活，当前无有效许可证时使用）
  update <file>       更新/续期许可证（已有许可证，替换为新的）
  deactivate [file]   解绑许可证，解除机器绑定以支持迁移
  status              查看许可证状态
  verify <file>       验证许可证文件（不激活）
  fingerprint         查看当前机器指纹和实例 ID

说明:
  activate 和 update 调用相同的 API，区别在于语义意图。
  服务端根据当前状态决定行为：无许可证时为激活，有许可证时为更新。

  deactivate 需要双因子验证：
    - 传入许可证文件路径: sciomnictl license deactivate /path/to/license.lic
    - 或自动使用本地离线密钥: sciomnictl license deactivate
      （需要 data/.offline-secret 文件存在）

LICEOF
            ;;
    esac
}

cmd_doctor() {
    log_info "Running diagnostics..."
    local issues=0

    if ! command -v docker &>/dev/null; then
        log_error "Docker not found"
        issues=$((issues + 1))
    else
        log_ok "Docker: $(docker --version)"
    fi

    if ! docker compose version &>/dev/null; then
        log_error "Docker Compose V2 not found"
        issues=$((issues + 1))
    else
        log_ok "Compose: $(docker compose version --short)"
    fi

    if ! docker info &>/dev/null 2>&1; then
        log_error "Docker daemon not running"
        issues=$((issues + 1))
    else
        log_ok "Docker daemon: running"
    fi

    local disk_usage
    disk_usage=$(df -h "$BASE_DIR" | awk 'NR==2{print $5}' | tr -d '%')
    if [ "$disk_usage" -gt 80 ]; then
        log_warn "Disk usage at ${disk_usage}% — consider cleanup"
        ((issues++))
    else
        log_ok "Disk usage: ${disk_usage}%"
    fi

    local sandbox_log_size
    sandbox_log_size=$(docker system df -v 2>/dev/null | grep "sciomni-sandbox-logs" | awk '{print $4}' || echo "0B")
    log_info "Sandbox log volume size: $sandbox_log_size"

    if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "sciomni-sandbox"; then
        local restart_count
        restart_count=$(docker inspect --format='{{.RestartCount}}' sciomni-sandbox 2>/dev/null || echo "0")
        if [ "$restart_count" -gt 3 ]; then
            log_warn "Sandbox container has restarted $restart_count times (possible crash loop)"
            ((issues++))
        fi
    fi

    if [ -f "$CONFIG_DIR/platform-config.json" ]; then
        local ext_auth_enabled
        ext_auth_enabled=$(jq -r '.external_auth.enabled // false' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
        if [ "$ext_auth_enabled" = "true" ]; then
            local provider
            provider=$(jq -r '.external_auth.provider // ""' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
            if [ "$provider" = "ldap" ]; then
                local ldap_url
                ldap_url=$(jq -r '.external_auth.ldap.url // empty' "$CONFIG_DIR/platform-config.json" 2>/dev/null)
                if [ -n "$ldap_url" ]; then
                    local ldap_host ldap_port
                    ldap_host=$(echo "$ldap_url" | sed 's|ldap[s]*://||;s|/.*||;s|:.*||')
                    ldap_port=$(echo "$ldap_url" | sed -n 's|^ldaps\?://[^/:]*:\([0-9]\+\).*$|\1|p')
                    ldap_port="${ldap_port:-389}"
                    if tcp_reachable "$ldap_host" "$ldap_port"; then
                        log_ok "LDAP/AD ($ldap_url): reachable"
                    else
                        log_warn "LDAP/AD ($ldap_url): unreachable"
                        ((issues++))
                    fi
                fi
            elif [ "$provider" = "nis" ]; then
                if command -v ypwhich &>/dev/null && ypwhich &>/dev/null 2>&1; then
                    log_ok "NIS domain: bound ($(ypwhich 2>/dev/null))"
                else
                    log_warn "NIS domain: not bound or ypwhich unavailable"
                    ((issues++))
                fi
            fi
        fi
    fi

    echo ""
    if [ "$issues" -eq 0 ]; then
        log_ok "All checks passed"
    else
        log_warn "$issues issue(s) found"
    fi
}

usage() {
    cat <<EOF
SciOmni Management Tool v$(get_version)

Usage: sciomnictl <command> [options]

Commands:
  preflight                       Pre-installation environment check
  install [--mode offline|online] [--version VER]
                                  First-time installation (offline by default)
  uninstall [--keep-data]         Uninstall (remove containers and volumes)
  start                           Start all services
  stop                            Stop all services
  restart [service]               Restart all or specific service
  status                          Show service status and worker info
  logs [service] [lines]          View service logs
  config validate                 Validate configuration (dry-run)
  config apply [--yes]            Apply configuration changes
  backup [--output PATH] [--db-only]  Create backup
  restore <backup-file>           Restore from backup
  upgrade --version VER [--from-file FILE] [--drain-timeout SEC]
                                  Upgrade to new version
  patch --service SVC --version VER
                                  Apply single-service patch
  rollback [--version VER]        Rollback to previous version
  drain-workers [--timeout SEC]   Drain workers (default 7200s)
  license <subcmd> [options]      License management (activate/status/verify/...)
  version                         Show version info
  doctor                          Run diagnostics

EOF
}

case "${1:-}" in
    preflight)  shift; cmd_preflight "$@" ;;
    install)    shift; acquire_lock; cmd_install "$@" ;;
    uninstall)  shift; acquire_lock; cmd_uninstall "$@" ;;
    start)      shift; acquire_lock; cmd_start "$@" ;;
    stop)       shift; acquire_lock; cmd_stop "$@" ;;
    restart)    shift; acquire_lock; cmd_restart "$@" ;;
    status)     shift; cmd_status "$@" ;;
    logs)       shift; cmd_logs "$@" ;;
    config)     shift; acquire_lock; cmd_config "$@" ;;
    backup)     shift; acquire_lock; cmd_backup "$@" ;;
    restore)    shift; acquire_lock; cmd_restore "$@" ;;
    upgrade)    shift; acquire_lock; cmd_upgrade "$@" ;;
    patch)      shift; acquire_lock; cmd_patch "$@" ;;
    rollback)   shift; acquire_lock; cmd_rollback "$@" ;;
    drain-workers) shift; acquire_lock; cmd_drain_workers "$@" ;;
    license)    shift; cmd_license "$@" ;;
    version)    shift; cmd_version "$@" ;;
    doctor)     shift; cmd_doctor "$@" ;;
    help|--help|-h) usage ;;
    *)          usage; exit 1 ;;
esac
