#!/usr/bin/env bash
# sema-up preflight — 单机预检(DEPLOY-OVERSEAS-PILOT §3d-2 报错规范:每个不可行都带原因+建议)。
# 在目标机上跑(bootstrap 会经 ssh 分发到每台机);人类可读报告到 stdout,--json 输出机器可读版
# (sema cloud deploy 渲染用,同一交互协议的 infeasible:{reason,suggestion} 字段)。
# 只读探测,零改动,幂等。退出码:0=可部署(可能有 WARN),1=存在 BLOCK 项。
set -u

JSON=false
PROFILE="full"   # full | data-center — data-center 档降低 RAM/盘要求
PORTS=""         # --ports=8090,5432,… 显式端口面(调用方按 profile+改口传;缺省按 profile 取默认)
PORTS_SET=false  # 显式给了 --ports(即使空串=「一个都不用检」,重入场景自己的口已剔除)——与「没给」区分
for a in "$@"; do
  case "$a" in
    --json) JSON=true ;;
    --profile=*) PROFILE="${a#--profile=}" ;;
    --ports=*) PORTS="${a#--ports=}"; PORTS_SET=true ;;
  esac
done
if [ -z "$PORTS" ] && ! $PORTS_SET; then
  # 只检本 profile 真会占的端口(data-center 检 8090/6443 会被无关服务误 BLOCK——真踩)
  case "$PROFILE" in
    data-center) PORTS="5432,9000" ;;
    *) PORTS="8090,5432,9000" ;;
  esac
fi

FINDINGS=() # 每条:级别|检查|结果|原因|建议
add() { FINDINGS+=("$1|$2|$3|$4|$5"); }

# ── OS / 发行版(D5:Debian 12/13、Ubuntu 22.04/24.04、Rocky 9/10;macOS=docker 档开发机场景,
#    npx -p @sema-agent/server sema-up 在 Mac 上跑 data-center/local-plus 是真需求)────────────────
OS_ID="unknown"; OS_VER=""
if [ "$(uname -s)" = "Darwin" ]; then
  OS_ID="macos"; OS_VER="$(sw_vers -productVersion 2>/dev/null || echo '?')"
fi
if [ -r /etc/os-release ]; then . /etc/os-release; OS_ID="${ID:-unknown}"; OS_VER="${VERSION_ID:-}"; fi
case "$OS_ID:$OS_VER" in
  debian:12*|debian:13*|ubuntu:22.04|ubuntu:24.04|rocky:9*|rocky:10*)
    add OK os "$OS_ID $OS_VER" "支持面内" "" ;;
  macos:*)
    if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
      add OK os "macOS $OS_VER (docker)" "开发机档:docker 后端可用(数据面/agent 跑在 Linux VM 里)" ""
    else
      add BLOCK os "macOS $OS_VER" "macOS 上需要可用的 docker(Docker Desktop/OrbStack)" \
        "装 Docker Desktop 或 OrbStack 后重跑"
    fi ;;
  *)
    add BLOCK os "$OS_ID $OS_VER" "不在支持面(Debian 12/13、Ubuntu 22.04/24.04、Rocky 9/10、macOS+docker)" \
      "换 OS,或 --os-unsupported 强制(自担风险)" ;;
esac
# SELinux/firewalld(Rocky 面):不 BLOCK,提示 bootstrap 会显式处理
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce 2>/dev/null)" = "Enforcing" ]; then
  add WARN selinux "Enforcing" "k3s 需要 selinux 适配包" "bootstrap 会装 k3s-selinux;如自装注意 rancher 官方指引"
fi

# ── 架构(镜像池 amd64-only;macOS docker 档只跑数据面/agent 容器,arm64 放行为 WARN)──────────
ARCH="$(uname -m)"
if [ "$ARCH" = "x86_64" ]; then add OK arch "$ARCH" "" ""
elif [ "$OS_ID" = "macos" ]; then add WARN arch "$ARCH" "沙箱镜像池 amd64-only;数据面/agent 镜像多架构可跑" "kata/沙箱池档换 x86_64 Linux 机"
else add BLOCK arch "$ARCH" "沙箱镜像池当前 amd64-only" "换 x86_64 机器;arm 支持在 backlog"; fi

# ── CPU / RAM / 磁盘:两档阈值(硬底线=无法部署才拦;推荐值=低于只建议不强求)────────────────
# full 推荐 4c/8G/80G(4C8G 能部署全部——clay 2026-07-12 拍);硬底线 2c/3G/40G(3G 依据=
# 全栈空载实测 571MiB(pg74+minio73+server104+registry190+gitea130,2026-07-12 vfy5 读数)
# +PG 缓存(RAM/4)+沙箱/任务运行余量;40G=沙箱镜像池+DB 的下限,再低装不下)。
# data-center 2c/2G/20G 底线。
CPUS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 0)"
if [ -r /proc/meminfo ]; then RAM_MB="$(awk '/MemTotal/{printf "%d", $2/1024}' /proc/meminfo)"
else RAM_MB="$(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%d", $1/1048576}')"; RAM_MB="${RAM_MB:-0}"; fi
if [ "$OS_ID" = "macos" ]; then DISK_GB="$(df -g / 2>/dev/null | awk 'NR==2{print $4}')"
else DISK_GB="$(df -BG --output=avail / 2>/dev/null | tail -1 | tr -dc '0-9')"; fi
if [ "$PROFILE" = "data-center" ]; then MIN_C=2 MIN_M=2048 MIN_D=20 REC_C=2 REC_M=4096 REC_D=40
else MIN_C=2 MIN_M=3072 MIN_D=40 REC_C=4 REC_M=8192 REC_D=80; fi
DOWNSIZE_HINT="①data-center 档(只数据面)②local-plus(agent 留本机)③外接第三方 S3/DB(--s3-endpoint/--pg-url,连不上会先检)④升配"
if [ "$CPUS" -lt "$MIN_C" ]; then add BLOCK cpu "${CPUS}c" "低于硬底线 ${MIN_C}c,确实无法稳定部署" "$DOWNSIZE_HINT"
elif [ "$CPUS" -lt "$REC_C" ]; then add WARN cpu "${CPUS}c" "低于推荐 ${REC_C}c(能跑,并发/沙箱吃紧)" "$DOWNSIZE_HINT"
else add OK cpu "${CPUS}c" "" ""; fi
if [ "$RAM_MB" -lt "$MIN_M" ]; then add BLOCK ram "$((RAM_MB/1024))G" "低于硬底线 $((MIN_M/1024))G(全栈空载实测 0.6G+PG 缓存+沙箱/任务余量,再低跑不稳)" "$DOWNSIZE_HINT"
elif [ "$RAM_MB" -lt "$REC_M" ]; then add WARN ram "$((RAM_MB/1024))G" "低于推荐 $((REC_M/1024))G(能跑,PG 缓存/沙箱余量小)" "$DOWNSIZE_HINT"
else add OK ram "$((RAM_MB/1024))G" "" ""; fi
if [ "${DISK_GB:-0}" -lt "$MIN_D" ]; then add BLOCK disk "${DISK_GB:-?}G avail /" "低于硬底线 ${MIN_D}G(沙箱镜像池+DB 装不下),无法推进" "挂数据盘/--data-root 指大盘;或外接 S3/DB 后按 data-center 底线重估"
elif [ "${DISK_GB:-0}" -lt "$REC_D" ]; then add WARN disk "${DISK_GB}G avail /" "低于推荐 ${REC_D}G(能跑,建议给数据留余量)" "①--data-root 指大盘(下方多盘扫描)②外接第三方 S3(--s3-endpoint)/DB(--pg-url)"
else add OK disk "${DISK_GB}G avail /" "" ""; fi

# ── 多盘扫描(自动选大容量盘的依据;只看真实块设备挂载,排 tmpfs/overlay/boot)──────────────
BEST_MNT="" BEST_GB=0
if [ "$OS_ID" = "macos" ]; then DF_OUT="$(df -g 2>/dev/null | awk '$1 ~ /^\/dev\// {print $9"\t"$4}')"
else DF_OUT="$(df -BG --output=source,avail,target 2>/dev/null | awk '$1 ~ /^\/dev\// {gsub(/G/,"",$2); print $3"\t"$2}')"; fi
while IFS="$(printf '\t')" read -r mnt avail; do
  [ -z "$mnt" ] && continue
  case "$mnt" in /boot*|/efi*|/System/*|/private/var/vm) continue ;; esac
  if [ "${avail:-0}" -gt "$BEST_GB" ] 2>/dev/null; then BEST_GB="$avail"; BEST_MNT="$mnt"; fi
done <<EOF_DF
$DF_OUT
EOF_DF
if [ -n "$BEST_MNT" ] && [ "$BEST_MNT" != "/" ] && [ "$BEST_GB" -gt "${DISK_GB:-0}" ]; then
  add WARN disk-alt "$BEST_MNT ${BEST_GB}G avail" "存在比 / 更大的数据盘(多盘机)" "--data-root=$BEST_MNT/sema-data(sema-up 未显式指定时会自动选它)"
fi

# ── KVM(kata lane 前提;无=不给选 kata,建议替代,不算 BLOCK——lane 可换) ───────────────────
if [ -e /dev/kvm ] && grep -qE 'vmx|svm' /proc/cpuinfo 2>/dev/null; then
  add OK kvm "available" "kata lane 可用(默认建议)" ""
else
  add WARN kvm "unavailable" "无虚拟化支持,kata lane 不可部署" \
    "①云上开嵌套虚拟化/换 bare metal ②e2b-cloud(填 key 即用)或 e2b-self ③runc(容器隔离,弱于 VM,不建议生产)"
fi

# ── 端口(按 --ports/profile 决定检查面) ──────────────────────────────────────────────────────
# 判定键=监听 socket 行存在(非进程名列:非 root 下 ss -p 看不到别人进程,root 服务会被误判 free);
# macOS 无 ss,用 lsof(只见得到本用户进程,兜底=compose 绑口 fail-loud)
for p in $(printf '%s' "$PORTS" | tr ',' ' '); do
  if command -v ss >/dev/null 2>&1; then
    LINE="$(ss -ltn 2>/dev/null | awk -v P=":$p\$" '$4 ~ P {print $4; exit}')"
    OWNER="$(ss -ltnp 2>/dev/null | awk -v P=":$p\$" '$4 ~ P {print $6; exit}')"
  else
    LINE="$(lsof -nP -iTCP:"$p" -sTCP:LISTEN 2>/dev/null | awk 'NR==2{print $9}')"
    OWNER="$(lsof -nP -iTCP:"$p" -sTCP:LISTEN 2>/dev/null | awk 'NR==2{print $1"/"$2}')"
  fi
  if [ -n "$LINE" ]; then
    add BLOCK "port-$p" "occupied" "被占用:$LINE ${OWNER:-(进程需 root 可见)}" "换口(--port 族)或停掉占用进程"
  else
    add OK "port-$p" "free" "" ""
  fi
done

# ── 已装检测(幂等重入=升级路径,不是重装) ─────────────────────────────────────────────────
command -v k3s >/dev/null 2>&1 && add WARN installed-k3s "$(k3s --version 2>/dev/null | head -1)" \
  "检测到已装 k3s" "bootstrap 走升级路径(helm upgrade),不会重装/清数据"
command -v docker >/dev/null 2>&1 && add OK installed-docker "$(docker --version 2>/dev/null | cut -d, -f1)" "已装,可复用" ""

# ── region 探测(cn|global;--region 可覆盖) ────────────────────────────────────────────────
t_cn=$(curl -m3 -so /dev/null -w '%{time_total}' https://mirrors.aliyun.com 2>/dev/null || echo 99)
t_gl=$(curl -m3 -so /dev/null -w '%{time_total}' https://registry.npmjs.org 2>/dev/null || echo 99)
REGION=$(awk -v a="$t_cn" -v b="$t_gl" 'BEGIN{print (a<b)?"cn":"global"}')
if [ "$t_cn" = "99" ] && [ "$t_gl" = "99" ]; then
  add BLOCK egress "none" "对外网络不通(aliyun/npmjs 双探全超时)" "检查出网/代理;air-gap 部署不在试点面"
else
  add OK region "$REGION" "aliyun=${t_cn}s npmjs=${t_gl}s" ""
fi

# ── 输出 ─────────────────────────────────────────────────────────────────────────────────────
BLOCKS=0
if $JSON; then
  printf '{"profile":"%s","findings":[' "$PROFILE"
  first=true
  for f in "${FINDINGS[@]}"; do
    IFS='|' read -r lv ck rs why fix <<<"$f"
    [ "$lv" = "BLOCK" ] && BLOCKS=$((BLOCKS+1))
    $first || printf ','
    first=false
    printf '{"level":"%s","check":"%s","result":"%s","reason":"%s","suggestion":"%s"}' \
      "$lv" "$ck" "$(printf '%s' "$rs" | sed 's/"/\\"/g')" "$(printf '%s' "$why" | sed 's/"/\\"/g')" "$(printf '%s' "$fix" | sed 's/"/\\"/g')"
  done
  printf '],"deployable":%s}\n' "$([ $BLOCKS -eq 0 ] && echo true || echo false)"
else
  printf '── sema-up preflight(profile=%s)──\n' "$PROFILE"
  for f in "${FINDINGS[@]}"; do
    IFS='|' read -r lv ck rs why fix <<<"$f"
    [ "$lv" = "BLOCK" ] && BLOCKS=$((BLOCKS+1))
    printf '[%-5s] %-16s %s' "$lv" "$ck" "$rs"
    [ -n "$why" ] && printf ' — %s' "$why"
    printf '\n'
    [ -n "$fix" ] && printf '        ↳ 建议:%s\n' "$fix"
  done
  if [ $BLOCKS -eq 0 ]; then printf '✅ 可部署(WARN 项 bootstrap 会显式处理)\n'
  else printf '⛔ %d 个 BLOCK 项,按建议处理后重跑\n' "$BLOCKS"; fi
fi
exit $([ $BLOCKS -eq 0 ] && echo 0 || echo 1)
