#!/usr/bin/env bash
# zcbot 日常部署脚本(Ubuntu / systemd)。详见 RUN.md §部署 SOP。
#
# 干什么(顺序写死,别动):
#   1) git pull --ff-only        拉新代码(工作区脏 / 非 ff 直接中止,不乱 merge)
#   2) pip install -r            幂等;requirements 没变 docker/pip 都秒过
#   3) db upgrade head           ★ alembic migration,已在 head 则 no-op
#   4) docker build sandbox      重建 run_python 沙箱镜像(layer cache:改代码秒过,
#                                requirements 变了才重下 chromium/字体/pip ~5-10min)
#   5) systemctl restart         ★ 必须在 4 之后 —— 重启时 shutdown_all 清旧容器,
#                                下次 ensure() 才用新镜像重建 per-user 容器
#   6) curl /healthz             验活
#
# 怎么跑:在部署机 `sudo deploy/update.sh`(需 root 跑 systemctl;文件操作降到 $APP_USER)。
#   --skip-build     跳过第 4 步 sandbox 镜像重建(host backend 机器 / 临时不想动 docker)
#   --prepare-only   只跑 1-4(拉码/依赖/migration/build),不 restart 不验活 ——
#                    蓝绿部署 update_bluegreen.sh 复用本脚本做准备,切换自己管
# 一次性 bootstrap(useradd / 写 systemd unit / enable)不在这里 —— 见 RUN.md §部署 一次性。
#
# 可调 env(不传则用默认 / Dockerfile 默认):
#   APP_DIR(默认=脚本上级目录,即仓库根)  APP_USER(默认=部署目录属主)  SERVICE(zcbot)  PORT(8765)
#   APT_MIRROR / PIP_INDEX_URL / PIP_TRUSTED_HOST / NPM_REGISTRY  镜像源,
#     默认:pip+apt 清华(tuna),npm 腾讯(清华无 npm 源;npmmirror 访问不稳,腾讯 npm 历来 OK)。
#     pip 选清华:腾讯 PyPI 曾给出损坏的 litellm wheel(index hash 对、文件字节不对 → pip "DO NOT
#     MATCH THE HASHES");清华境内稳 + 新包同步及时(阿里 PyPI 曾滞后到没有 litellm>=1.83)。
#     注:坏 wheel 只是腾讯 PyPI 的事,腾讯 npm 不受影响。
#     要稳定命中 docker cache 就别在多组源之间来回换(换源会从 pip 层炸开全重跑)。
#     想用官方源:PIP_INDEX_URL= sudo -E bash ...(置空)

set -euo pipefail

# ── 参数 ──────────────────────────────────────────────────────
# 默认 build;--skip-build 显式跳过(不为旧用法留 env 别名,开发期不留兼容)。
# 原始参数先存一份 —— pull 后若脚本自身有更新要 exec 新版本重跑,得把参数原样传回。
ORIG_ARGS=("$@")
SKIP_BUILD=0
PREPARE_ONLY=0
while [ $# -gt 0 ]; do
    case "$1" in
        --skip-build) SKIP_BUILD=1; shift ;;
        --prepare-only) PREPARE_ONLY=1; shift ;;
        -h|--help) echo "用法:sudo $0 [--skip-build] [--prepare-only]"; exit 0 ;;
        *) echo "[deploy] ERROR: 未知参数:$1(只支持 --skip-build / --prepare-only)" >&2; exit 1 ;;
    esac
done

# 部署目录 = 本脚本所在目录(deploy/)的上一级,不写死路径;仍可 env 覆盖。
# readlink -f 解析软链 + 相对路径,兼容 `sudo ./update.sh` / `sudo bash /abs/deploy/update.sh`
# / 经软链调用(Ubuntu coreutils 有 readlink -f)。
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
APP_DIR="${APP_DIR:-$(dirname "$SCRIPT_DIR")}"
# 服务用户默认 = 部署目录属主(bootstrap 时 chown zcbot:zcbot /opt/zcbot,属主即服务用户),
# 不写死;可 env 覆盖。推成 root 见下方前置检查 warn。
APP_USER="${APP_USER:-$(stat -c %U "$APP_DIR")}"
SERVICE="${SERVICE:-zcbot}"
PORT="${PORT:-8765}"
IMAGE="zcbot-sandbox:latest"
PY="$APP_DIR/.venv/bin/python"
HEALTH_URL="http://127.0.0.1:${PORT}/healthz"

# 镜像源默认:pip+apt 清华(tuna,境内稳 + 同步及时;Dockerfile sed 只替 host 前缀,目录结构同官方),
# npm 用腾讯(清华不提供 npm registry;npmmirror 访问不稳被弃,腾讯 npm 历来 OK)。
# pip 选清华:腾讯 PyPI 曾返回损坏的 litellm wheel(hash 不匹配),阿里 PyPI 又一度滞后(litellm 只到 1.82.6)。
# 备选 npm 源:华为 https://repo.huaweicloud.com/repository/npm/ / USTC https://npmreg.mirrors.ustc.edu.cn/
# 置空(如 PIP_INDEX_URL= sudo -E ...)则该项不传 build-arg,回落 Dockerfile 官方源默认。
APT_MIRROR="${APT_MIRROR-https://mirrors.tuna.tsinghua.edu.cn}"
PIP_INDEX_URL="${PIP_INDEX_URL-https://pypi.tuna.tsinghua.edu.cn/simple/}"
NPM_REGISTRY="${NPM_REGISTRY-https://mirrors.cloud.tencent.com/npm/}"

log()  { echo "[deploy] $*"; }
fail() { echo "[deploy] ERROR: $*" >&2; exit 1; }

# 文件操作以 $APP_USER 身份跑(git/pip/db/docker 都碰 /opt/zcbot,保持 owner 一致)
asuser() { sudo -u "$APP_USER" "$@"; }

# ── 0) 前置检查 ────────────────────────────────────────────────
[ "$(id -u)" -eq 0 ] || fail "需 root 跑 systemctl,请用:sudo $0"
[ -d "$APP_DIR/.git" ] || fail "$APP_DIR 不是 git 仓库(部署目录不对?)"
[ -x "$PY" ] || fail "找不到 venv python:$PY"
[ -f "$APP_DIR/.env" ] || fail "缺 $APP_DIR/.env(ZCBOT_DB_URL 等)"
# 服务用户落到 root 八成是部署目录属主不对(systemd 服务 / 沙箱不该跑 root,
# HOST_UID 还要跟 bind mount 属主对齐);提醒但不强制中止
[ "$APP_USER" = "root" ] && log "WARN: APP_USER 推导为 root(部署目录属主是 root?)── 确认服务确实以 root 跑;否则 APP_USER=zcbot sudo -E bash $0"
id "$APP_USER" >/dev/null 2>&1 || fail "服务用户不存在:$APP_USER(检查部署目录属主 / 显式设 APP_USER)"

# 工作区脏(仅看已跟踪文件;.venv/.env/workspace 都 gitignore,不算)→ 中止,
# 避免 ff-only 失败或把本地手改卷进部署
if [ -n "$(asuser git -C "$APP_DIR" status --porcelain -uno)" ]; then
    asuser git -C "$APP_DIR" status --short -uno
    fail "工作区有未提交改动,先处理干净再部署"
fi

# ── 1) 拉代码 ─────────────────────────────────────────────────
OLD_HEAD="$(asuser git -C "$APP_DIR" rev-parse HEAD)"
log "git pull --ff-only ..."
asuser git -C "$APP_DIR" pull --ff-only
NEW_HEAD="$(asuser git -C "$APP_DIR" rev-parse HEAD)"

# 脚本改自己的坑:正在跑的是 pull 前的旧脚本(变量默认值早在顶部求值,bash 又按字节
# 偏移边读边跑),pull 进来的新 update.sh 这一轮不生效。若这次 pull 动了本脚本,exec
# 新版本从头重跑(原始参数原样传回),ZCBOT_UPDATE_REEXEC 标记防死循环。pull 幂等,
# 重跑里它变 no-op,只多一次 git 往返,代价可忽略。
if [ "$OLD_HEAD" != "$NEW_HEAD" ] && [ "${ZCBOT_UPDATE_REEXEC:-0}" != "1" ] \
   && ! asuser git -C "$APP_DIR" diff --quiet "$OLD_HEAD" "$NEW_HEAD" -- deploy/update.sh; then
    log "update.sh 自身有更新 —— 用新版本重跑(避免旧脚本跑出过期行为)..."
    exec env ZCBOT_UPDATE_REEXEC=1 bash "$APP_DIR/deploy/update.sh" "${ORIG_ARGS[@]}"
fi

if [ "$OLD_HEAD" = "$NEW_HEAD" ]; then
    log "代码无更新($NEW_HEAD),仍继续 pip/migration/build 兜底幂等"
else
    log "新提交 $OLD_HEAD -> $NEW_HEAD:"
    asuser git -C "$APP_DIR" --no-pager log --oneline "$OLD_HEAD..$NEW_HEAD" || true
fi

# ── 2) 依赖 ───────────────────────────────────────────────────
# host venv 的 pip 也走同一镜像源 —— 经 sudo -u 后 PIP_INDEX_URL env 会被洗掉,
# 不显式 --index-url 的话 host 用自己的 pip.conf(可能仍指阿里,撞 litellm>=1.83 缺版本)。
# 带 -q:日常部署依赖基本没变,全量 "Requirement already satisfied" 纯刷屏;
# 真装新包 / 装挂了(报错)仍会输出,不影响排障。
pip_args=(-q -r "$APP_DIR/requirements.txt" --timeout 60)
[ -n "${PIP_INDEX_URL:-}" ]    && pip_args+=(--index-url "${PIP_INDEX_URL}")
[ -n "${PIP_TRUSTED_HOST:-}" ] && pip_args+=(--trusted-host "${PIP_TRUSTED_HOST}")
log "pip install -r requirements.txt(源:${PIP_INDEX_URL:-官方默认})..."
asuser "$PY" -m pip install "${pip_args[@]}"

# ── 3) DB migration ──────────────────────────────────────────
# env.py 直接读 os.environ['ZCBOT_DB_URL'](不读 .env),这里从 .env 抠出来显式喂进去。
# 不整体 source .env —— 避免把 .env 当 shell 执行(值里若有 $(...) 会被求值)。
DB_URL="$(grep -E '^[[:space:]]*ZCBOT_DB_URL=' "$APP_DIR/.env" | tail -n1 | cut -d= -f2-)"
[ -n "$DB_URL" ] || fail ".env 里没读到 ZCBOT_DB_URL"
log "db upgrade head ..."
asuser env ZCBOT_DB_URL="$DB_URL" "$PY" "$APP_DIR/main.py" db upgrade head

# ── 4) 重建 sandbox 镜像 ──────────────────────────────────────
if [ "$SKIP_BUILD" = "1" ]; then
    log "--skip-build,跳过 sandbox 镜像重建"
else
    # build-arg 稳定才命中 cache:HOST_UID/GID 跟 host $APP_USER 对齐(bind mount owner),
    # 镜像源仅在对应 env 非空时才传(传空会把 Dockerfile 默认覆盖成空串,pip 直接挂)。
    build_args=(
        --build-arg "HOST_UID=$(id -u "$APP_USER")"
        --build-arg "HOST_GID=$(id -g "$APP_USER")"
    )
    [ -n "${APT_MIRROR:-}" ]       && build_args+=(--build-arg "APT_MIRROR=${APT_MIRROR}")
    [ -n "${PIP_INDEX_URL:-}" ]    && build_args+=(--build-arg "PIP_INDEX_URL=${PIP_INDEX_URL}")
    [ -n "${PIP_TRUSTED_HOST:-}" ] && build_args+=(--build-arg "PIP_TRUSTED_HOST=${PIP_TRUSTED_HOST}")
    [ -n "${NPM_REGISTRY:-}" ]     && build_args+=(--build-arg "NPM_REGISTRY=${NPM_REGISTRY}")

    # 进度走 docker 默认(TTY 下是分层折叠刷新的 UI,直观);不强制 plain。
    log "docker build $IMAGE(改代码秒过 / requirements 变了 ~5-10min)..."
    asuser docker build \
        -f "$APP_DIR/deploy/sandbox/Dockerfile" \
        "${build_args[@]}" \
        -t "$IMAGE" \
        "$APP_DIR"
fi

# ── 4.5) prepare-only:蓝绿部署由 update_bluegreen.sh 接管切换 ──
if [ "$PREPARE_ONLY" = "1" ]; then
    log "--prepare-only:代码/依赖/migration/镜像已就绪($NEW_HEAD),不 restart"
    exit 0
fi

# ── 5) 重启 service(在 build 之后!)───────────────────────────
# 优雅 drain:若有在跑的 run,restart 会先等它们收尾(≤ unit TimeoutStopSec,
# 见 RUN.md / config/agent.yaml shutdown 段)再换新版 —— 这步"看着卡住"几十秒是正常的。
log "systemctl restart $SERVICE(有在跑的 run 会先 drain，可能等几十秒)..."
systemctl restart "$SERVICE"

# ── 6) 验活 ───────────────────────────────────────────────────
log "等 /healthz 起来 ..."
ok=0
for i in $(seq 1 15); do
    if curl -fsS --max-time 2 "$HEALTH_URL" 2>/dev/null | grep -q '"ok"'; then
        ok=1
        break
    fi
    sleep 1
done

systemctl --no-pager --lines=0 status "$SERVICE" || true
if [ "$ok" -ne 1 ]; then
    echo "[deploy] ----- journalctl -u $SERVICE -n 50 -----" >&2
    journalctl -u "$SERVICE" -n 50 --no-pager >&2 || true
    fail "$HEALTH_URL 15s 内没返回 ok,service 可能没起干净(看上面日志)"
fi

log "OK:healthz 通过,部署完成($NEW_HEAD)"
log "看实时日志:journalctl -u $SERVICE -f"
