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

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOST="${STUDIO_HOST:-58.38.22.103}"
REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}"
APP_DIR="${REMOTE_ROOT}/Memind"
INCOMING_DIR="${REMOTE_ROOT}/incoming/memind-portal-runtime"
BACKUP_DIR="${REMOTE_ROOT}/backups/memind"
ARCHIVE_DIR="${REMOTE_ROOT}/archives"
HEALTH_URL="${STUDIO_HEALTH_URL:-http://127.0.0.1:8081/api/status}"
PORTAL_LABEL="cn.tkmind.memind-portal"
LAUNCHD_GUI="gui/$(id -u)"
RELEASE_TS="$(date +%Y%m%d-%H%M%S)"
SHORT_SHA="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo no-git)"
RELEASE_ID="${RELEASE_TS}-${SHORT_SHA}"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/memind-portal-runtime-release.XXXXXX")"
RUNTIME_ROOT="${ROOT}/.runtime/portal"
BUNDLE_PATH="${TMP_DIR}/memind-portal-runtime-${RELEASE_ID}.tar.gz"
MANIFEST_PATH="${TMP_DIR}/memind-portal-runtime-${RELEASE_ID}.manifest.txt"
SHA_PATH="${TMP_DIR}/memind-portal-runtime-${RELEASE_ID}.sha256"
DRY_RUN=0
SKIP_TESTS=0
SKIP_BUILD=0
AUTO_YES=0

cleanup() {
  rm -rf "${TMP_DIR}"
}
trap cleanup EXIT

usage() {
  cat <<'EOF'
用法:
  bash scripts/release-portal-runtime-prod.sh [--dry-run] [--skip-tests] [--skip-build] [--yes]

说明:
  1. 本地构建 Portal 无源码 runtime artifact
  2. 上传到 103（固定公网地址）
  3. 103 备份当前 Memind 全目录 + 持久目录
  4. 停止旧 Portal 服务
  5. 用 runtime artifact 替换 live 目录，只继承持久目录
  6. 启动 Portal，保持旧端口 8081
  7. 健康检查通过后，旧源码目录移入 archive，不再保留可运行 live 源码
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --dry-run) DRY_RUN=1 ;;
    --skip-tests) SKIP_TESTS=1 ;;
    --skip-build) SKIP_BUILD=1 ;;
    --yes|-y) AUTO_YES=1 ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "未知参数: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
  shift
done

say() {
  printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
}

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || {
    echo "缺少命令: $1" >&2
    exit 1
  }
}

need_cmd ssh
need_cmd scp
need_cmd tar
need_cmd shasum

say "本地预检查"
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo portal-runtime-ssh-ok" >/dev/null
ssh -o BatchMode=yes "${HOST}" "test -d '${APP_DIR}' && test -f '${APP_DIR}/.env'" >/dev/null

if [[ "${SKIP_TESTS}" -ne 1 ]]; then
  say "运行最小验证"
  (
    cd "${ROOT}"
    npm test -- --test-name-pattern='publish|space|billing' >/dev/null
  )
fi

if [[ "${SKIP_BUILD}" -ne 1 ]]; then
  say "构建 Portal runtime"
  (
    cd "${ROOT}"
    node scripts/build-portal-runtime.mjs
  )
fi

[[ -d "${RUNTIME_ROOT}" ]] || {
  echo "缺少 runtime 目录: ${RUNTIME_ROOT}" >&2
  exit 1
}

verify_runtime_artifact() {
  local missing=0
  for required in server.mjs mindspace-sandbox-mcp.mjs dist package.json scripts/run-memind-portal-prod.sh; do
    if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
      echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
      missing=1
    fi
  done
  if [[ "${missing}" -ne 0 ]]; then
    echo "请先执行: node scripts/build-portal-runtime.mjs" >&2
    exit 1
  fi
}

verify_runtime_artifact

if [[ "${AUTO_YES}" -ne 1 && "${DRY_RUN}" -ne 1 ]]; then
  say "发布确认"
  echo "目标主机: ${HOST}"
  echo "目标目录: ${APP_DIR}"
  echo "发布编号: ${RELEASE_ID}"
  echo "本地 HEAD: $(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
  echo "说明: 此次只切换 test-memind Portal 到无源码 runtime，不包含 memindadm / memindplaza"
  read -r -p "确认继续发布到 103? [y/N] " confirm </dev/tty
  [[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
fi

say "生成发布清单"
{
  echo "release_id=${RELEASE_ID}"
  echo "created_at=$(date '+%Y-%m-%d %H:%M:%S %z')"
  echo "host=$(hostname)"
  echo "git_head=$(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
  echo "git_branch=$(git -C "${ROOT}" branch --show-current 2>/dev/null || echo detached)"
  echo "artifact=.runtime/portal"
  echo "persisted_items=.env, MindSpace, data, users, .tailscale, public/plaza-covers, logs"
} > "${MANIFEST_PATH}"

say "打包 Portal runtime artifact"
tar -czf "${BUNDLE_PATH}" -C "${RUNTIME_ROOT}" .
checksum="$(shasum -a 256 "${BUNDLE_PATH}" | awk '{print $1}')"
printf '%s  %s\n' "${checksum}" "$(basename "${BUNDLE_PATH}")" > "${SHA_PATH}"

if [[ "${DRY_RUN}" -eq 1 ]]; then
  say "dry-run 完成"
  ls -lh "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}"
  exit 0
fi

say "上传 Portal runtime 到 103"
ssh -o BatchMode=yes "${HOST}" "mkdir -p '${INCOMING_DIR}' '${BACKUP_DIR}' '${ARCHIVE_DIR}'"
scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR}/"

say "在 103 停旧服务并切换到无源码 runtime"
ssh -o BatchMode=yes "${HOST}" \
  "RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' HEALTH_URL='${HEALTH_URL}' PORTAL_LABEL='${PORTAL_LABEL}' LAUNCHD_GUI='${LAUNCHD_GUI}' /bin/bash" <<'REMOTE_SCRIPT'
set -euo pipefail

RUNTIME_DIR="${APP_DIR}.runtime-${RELEASE_ID}"
OLD_LIVE_DIR="${ARCHIVE_DIR}/Memind-source-before-${RELEASE_ID}"
BUNDLE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.tar.gz"
MANIFEST="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.manifest.txt"
SHA_FILE="${INCOMING_DIR}/memind-portal-runtime-${RELEASE_ID}.sha256"
FULL_BACKUP_TAR="${BACKUP_DIR}/memind-full-${RELEASE_ID}-before.tar.gz"
PERSIST_BACKUP_TAR="${BACKUP_DIR}/memind-persisted-${RELEASE_ID}-before.tar.gz"
PERSISTED_ITEMS=(
  ".env"
  "MindSpace"
  "data"
  "users"
  ".tailscale"
  "public/plaza-covers"
  "logs"
)

say() {
  printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
}

rollback() {
  if [[ -d "${OLD_LIVE_DIR}" && ! -d "${APP_DIR}" ]]; then
    mv "${OLD_LIVE_DIR}" "${APP_DIR}"
  fi
  launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" >/dev/null 2>&1 || true
  launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true
  if ! curl -sf "${HEALTH_URL}" >/dev/null 2>&1; then
    nohup "${APP_DIR}/scripts/run-memind-portal-prod.sh" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 &
  fi
}

trap 'rollback' ERR

[[ -f "${BUNDLE}" ]] || { echo "missing bundle: ${BUNDLE}" >&2; exit 1; }
[[ -f "${MANIFEST}" ]] || { echo "missing manifest: ${MANIFEST}" >&2; exit 1; }
[[ -f "${SHA_FILE}" ]] || { echo "missing sha file: ${SHA_FILE}" >&2; exit 1; }

say "校验产物完整性"
cd "${INCOMING_DIR}"
shasum -a 256 -c "$(basename "${SHA_FILE}")"

say "备份当前 live 全目录"
COPYFILE_DISABLE=1 tar --exclude='Memind/.tailscale/*.sock' \
  -czf "${FULL_BACKUP_TAR}" -C "$(dirname "${APP_DIR}")" "$(basename "${APP_DIR}")"

say "单独备份持久目录"
tmp_persist_dir="$(mktemp -d "${TMPDIR:-/tmp}/memind-persisted.XXXXXX")"
for item in "${PERSISTED_ITEMS[@]}"; do
  if [[ -e "${APP_DIR}/${item}" ]]; then
    mkdir -p "${tmp_persist_dir}/$(dirname "${item}")"
    if [[ "${item}" == ".tailscale" && -d "${APP_DIR}/${item}" ]]; then
      mkdir -p "${tmp_persist_dir}/${item}"
      (
        cd "${APP_DIR}/${item}"
        COPYFILE_DISABLE=1 tar --exclude='*.sock' -cf - .
      ) | (
        cd "${tmp_persist_dir}/${item}"
        tar -xf -
      )
    else
      cp -a "${APP_DIR}/${item}" "${tmp_persist_dir}/$(dirname "${item}")/"
    fi
  fi
done
COPYFILE_DISABLE=1 tar -czf "${PERSIST_BACKUP_TAR}" -C "${tmp_persist_dir}" .
rm -rf "${tmp_persist_dir}"

say "停止旧 Portal 服务"
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true
lsof -tiTCP:8081 -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
sleep 2
lsof -tiTCP:8081 -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true

say "准备新的 runtime live 目录"
rm -rf "${RUNTIME_DIR}"
mkdir -p "${RUNTIME_DIR}"
tar -xzf "${BUNDLE}" -C "${RUNTIME_DIR}"
cp "${MANIFEST}" "${RUNTIME_DIR}/.release-manifest.txt"

for item in "${PERSISTED_ITEMS[@]}"; do
  if [[ -e "${APP_DIR}/${item}" ]]; then
    rm -rf "${RUNTIME_DIR:?}/${item}"
    mkdir -p "$(dirname "${RUNTIME_DIR}/${item}")"
    if [[ "${item}" == ".tailscale" && -d "${APP_DIR}/${item}" ]]; then
      mkdir -p "${RUNTIME_DIR}/${item}"
      (
        cd "${APP_DIR}/${item}"
        COPYFILE_DISABLE=1 tar --exclude='*.sock' -cf - .
      ) | (
        cd "${RUNTIME_DIR}/${item}"
        tar -xf -
      )
    else
      cp -a "${APP_DIR}/${item}" "$(dirname "${RUNTIME_DIR}/${item}")/"
    fi
  fi
done

say "将旧源码 live 目录移入 archive"
rm -rf "${OLD_LIVE_DIR}"
mv "${APP_DIR}" "${OLD_LIVE_DIR}"
mv "${RUNTIME_DIR}" "${APP_DIR}"

say "更新 LaunchAgent 指向 runtime 启动脚本"
cat > "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${PORTAL_LABEL}</string>
  <key>ProgramArguments</key>
  <array>
    <string>${APP_DIR}/scripts/run-memind-portal-prod.sh</string>
  </array>
  <key>WorkingDirectory</key>
  <string>${APP_DIR}</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin</string>
  </dict>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ThrottleInterval</key>
  <integer>10</integer>
  <key>StandardOutPath</key>
  <string>${HOME}/Library/Logs/memind-portal.log</string>
  <key>StandardErrorPath</key>
  <string>${HOME}/Library/Logs/memind-portal.log</string>
</dict>
</plist>
EOF

say "启动新的 Portal runtime"
launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" >/dev/null 2>&1 || true
launchctl enable "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true
if ! launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1; then
  nohup "${APP_DIR}/scripts/run-memind-portal-prod.sh" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 &
fi

say "健康检查"
for _ in $(seq 1 60); do
  portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)"
  if [[ "${portal_code}" == "200" ]]; then
    break
  fi
  sleep 2
done

portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)"
if [[ "${portal_code}" != "200" ]]; then
  echo "health check failed: portal=${portal_code}" >&2
  exit 1
fi

say "检查 live 目录中不再保留源码树"
allowed_live_mjs=(
  "${APP_DIR}/server.mjs"
  "${APP_DIR}/mindspace-sandbox-mcp.mjs"
)
extra_files=""
while IFS= read -r file; do
  [[ -z "${file}" ]] && continue
  allowed=0
  for allowed_file in "${allowed_live_mjs[@]}"; do
    if [[ "${file}" == "${allowed_file}" ]]; then
      allowed=1
      break
    fi
  done
  if [[ "${allowed}" -eq 0 ]]; then
    extra_files+="${file} "
  fi
done < <(find "${APP_DIR}" -maxdepth 1 \( -name '*.mjs' -o -name 'docs' -o -name 'src' -o -name 'ops' \))
if [[ -n "${extra_files// /}" ]]; then
  echo "unexpected source-like files remain in live dir: ${extra_files}" >&2
  exit 1
fi

say "Portal runtime 发布完成"
printf 'live_dir=%s\n' "${APP_DIR}"
printf 'archived_source=%s\n' "${OLD_LIVE_DIR}"
printf 'full_backup=%s\n' "${FULL_BACKUP_TAR}"
printf 'persist_backup=%s\n' "${PERSIST_BACKUP_TAR}"
REMOTE_SCRIPT

say "读取 103 最终状态"
ssh -o BatchMode=yes "${HOST}" "curl -s '${HEALTH_URL}' && echo && ls -la '${APP_DIR}' | sed -n '1,80p' && echo '---' && ls -la '${APP_DIR}/scripts' | sed -n '1,40p'"
