#!/usr/bin/env bash
# PAC 生产部署(确定性,不信 compose 的 diff 启发式)
#
# 背景:docker compose `up -d --build` 存在跨版本反复复发的已知缺陷 —— 镜像构建了但
# 运行中容器不切新镜像(docker/compose#9308 #9259 等;本项目 2026-06-10 实测中招:
# 服务还跑旧镜像 → migrate 报 "no pending" → 新列没建)。
# 根治:生产部署不依赖 compose 的"要不要重建"判定 —— 显式 build → 显式 force-recreate
# → 部署后硬验证(容器镜像 ID == 新镜像 ID / 迁移无 pending / health 200),任一不过即失败退出。
#
# ⚠️ 整个逻辑包在 main() 里,文件末尾才调用 —— bash 是边读边执行脚本文件的,
#    而本脚本会 git pull 更新自己;不包 main 的话,pull 后 bash 继续按"新文件的字节偏移"
#    读后半段 → 执行旧/错位代码(2026-06-10 实测:验证逻辑跑的是旧版)。
#
# 用法(服务器上,~/pac):
#   bash deploy/deploy-prod.sh            # git pull + build + 切换 + 验证
#   bash deploy/deploy-prod.sh --no-pull  # 跳过 git pull(代码已就位时)
set -euo pipefail

log() { printf '\n\033[1;36m== %s ==\033[0m\n' "$*"; }
die() { printf '\033[1;31mFAIL: %s\033[0m\n' "$*" >&2; exit 1; }

main() {
  cd "$(dirname "$0")/.."

  # 托管 DB/Redis:COMPOSE_MANAGED=1 叠加 override(不启自带 PG/Redis,URL 走 .env 的托管端点)
  local COMPOSE=(docker compose -f docker-compose.prod.yml)
  [[ "${COMPOSE_MANAGED:-0}" == "1" ]] && COMPOSE+=(-f docker-compose.managed.yml)
  COMPOSE+=(--env-file apps/pac-service/.env --env-file apps/pac-web/.env)
  local SERVICES=(pac-migrate pac-service pac-web)
  # 语音 ASR 可选(compose profile=asr),默认不部署;要语音助手用 WITH_ASR=1 bash deploy/deploy-prod.sh
  [[ "${WITH_ASR:-0}" == "1" ]] && SERVICES+=(pac-asr)

  if [[ "${1:-}" != "--no-pull" ]]; then
    log "git pull --ff-only"
    git pull --ff-only origin main
    # pull 可能更新了本脚本(SERVICES 列表/验证逻辑)→ re-exec 新版执行后续步骤,
    # 否则本轮跑的是 pull 前加载的旧版(2026-06-10 实测:新加的 pac-asr 服务没被 build)
    log "re-exec 最新脚本(--no-pull)"
    exec bash "$0" --no-pull
  fi
  log "HEAD: $(git log --oneline -1)"

  log "build 镜像(显式,不和 up 混)"
  "${COMPOSE[@]}" build "${SERVICES[@]}"

  log "force-recreate(不信 compose 的重建判定)"
  "${COMPOSE[@]}" up -d --force-recreate "${SERVICES[@]}"

  # ── 部署后硬验证:任一不过 = 部署失败 ─────────────────────────────
  log "验证 1/3:容器跑的镜像 == 刚构建的镜像"
  local proj svc want got
  proj=$(basename "$PWD")
  for svc in pac-service pac-web; do
    want=$(docker image inspect "${proj}-${svc}" --format '{{.Id}}')
    got=$(docker inspect "${proj}-${svc}-1" --format '{{.Image}}')
    [[ "$want" == "$got" ]] || die "$svc 容器仍在旧镜像(want ${want:7:12} got ${got:7:12})— compose 又没重建"
    echo "  $svc OK (${want:7:12})"
  done

  log "验证 2/3:数据库迁移无 pending(migrate 容器 exit 0 且 deploy 干净)"
  local mexit status_out
  mexit=$(docker inspect "${proj}-pac-migrate-1" --format '{{.State.ExitCode}}')
  [[ "$mexit" == "0" ]] || die "migrate 容器 exit=$mexit(看 docker logs ${proj}-pac-migrate-1)"
  # 注 1:prisma 的"新版本提示框"位置不固定(异步)→ 不能 tail 截断,全量输出里找;
  # 注 2:pipefail 下 `cmd | grep -q` 会因 grep 提前退出给 cmd 发 SIGPIPE → 管道 141 误判失败
  #       → 先落变量再 grep。
  status_out=$(docker exec "${proj}-pac-service-1" sh -c 'npx prisma migrate status 2>&1' || true)
  grep -qiE 'database schema is up to date' <<<"$status_out" \
    || die "迁移有 pending / 状态异常: $(tail -2 <<<"$status_out")"
  echo "  migrate OK"

  log "验证 3/3:health + web"
  local i code webcode
  for i in $(seq 1 30); do
    code=$(curl -s -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:3101/pac/v1/health || true)
    [[ "$code" == "200" ]] && break
    sleep 2
  done
  [[ "${code:-}" == "200" ]] || die "service health=$code"
  # / 现为 307→/plans(next.config redirects),直接验 /plans 的 200
  webcode=$(curl -s -m 30 -o /dev/null -w '%{http_code}' http://127.0.0.1:3100/plans || true)
  [[ "$webcode" == "200" ]] || die "web http=$webcode"
  echo "  health 200 / web 200(loopback)"

  # 托管环境(COMPOSE_MANAGED=1)= 远端网关(如 friday 的 lb1)经【ECS 私网 IP】到达应用。
  # loopback 验证过不代表网关能到:若应用绑成 127.0.0.1,私网 IP 会 refused → 公网 502。
  # 这里额外验私网 IP 可达,把"loopback 通、网关不通"的漂移在部署当场拦下(2026-07-18 踩过)。
  if [[ "${COMPOSE_MANAGED:-0}" == "1" ]]; then
    log "验证 3b:私网 IP 可达(网关路径,防 loopback-only 绑定)"
    local priv phealth pweb
    priv=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1);exit}}')
    [[ -n "$priv" ]] || die "拿不到私网 IP(ip route get 1 解析失败)"
    phealth=$(curl -s -m 5 -o /dev/null -w '%{http_code}' "http://$priv:3101/pac/v1/health" || true)
    [[ "$phealth" == "200" ]] || die "私网 $priv:3101 health=$phealth — 应用可能绑成 loopback,网关够不到(会 502)。托管环境需 web/service 绑 0.0.0.0(见 docker-compose.managed.yml)"
    pweb=$(curl -s -m 30 -o /dev/null -w '%{http_code}' "http://$priv:3100/plans" || true)
    [[ "$pweb" == "200" ]] || die "私网 $priv:3100 web=$pweb — 同上(loopback 绑定 → 网关 502)"
    echo "  私网 $priv health 200 / web 200(网关路径 OK)"
  fi

  log "部署成功 ✅  $(git log --oneline -1)"
}

main "$@"
