#!/usr/bin/env bash
# ============================================================
# 通用一键发布 extension 到阿里云 FC（persona 无关）
# 用法: publish.sh <extension目录> <persona根目录> <env>
#   - <persona根目录>: daemon 按 session persona 解析后传入（.../personas/<id>）
#   - <env>: integration | prod（必填，无默认）。integration 发到 <APP_NAME>-<SLUG>-int（测试地址），
#     prod 发到 <APP_NAME>-<SLUG>（老地址不变）。见 spec 2026-09-15-app-builder-integration-env-design
#   - 脚本骨架/bootstrap 来自本脚本所在的 deploy-kit（persona 目录 mcp/clawd-app-builder/ 下）；平台凭证在进程 env
#   - persona 注入值/占位符/特化逻辑/persona 密钥来自 <persona根>/extension-kit
# ============================================================
set -euo pipefail

KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"            # deploy-kit 根（persona 目录 mcp/clawd-app-builder/deploy-kit/）
EXT_DIR="${1:?用法: publish.sh <extension目录> <persona根目录> <env>}"
EXT_DIR="$(cd "$EXT_DIR" && pwd)"
PERSONA_ROOT="${2:?缺 persona 根目录}"
PERSONA_KIT="$PERSONA_ROOT/extension-kit"

# 1) 配置：persona config.env（含平台值+注入值）+ extension 自身 ext.conf（覆盖）
[ -f "$PERSONA_KIT/config.env" ] && source "$PERSONA_KIT/config.env"
[ -f "$EXT_DIR/ext.conf" ] && source "$EXT_DIR/ext.conf"
# env 在 source 之后赋值：位置参数是唯一来源，config.env / ext.conf 里就算写了同名变量也盖不过它
APP_ENV="${3:?缺 env（integration|prod）}"
case "$APP_ENV" in integration|prod) ;; *) echo "❌ env 只能是 integration|prod，收到：$APP_ENV" >&2; exit 1;; esac
APP_NAME="${APP_NAME:-$(basename "$EXT_DIR")}"
CODE_DIR="${CODE_DIR:-./server}"
FC_ENTRY="${FC_ENTRY:-dist/main.js}"
BUILD_CMD="${BUILD_CMD:-}"

# 全局唯一身份:APP_NAME (人读身份/表前缀根) + SLUG (4 位散列)。一次性 scaffold 时
# 由 new-extension.sh 锁定 + bake 进 ext.conf。这里的 fallback 兜底两种历史情况:
#   1) 老项目 (本改动前 scaffold) ext.conf 无 SLUG → 现场生成 + bake,保后续稳定
#   2) 手动 rm SLUG 或损坏 ext.conf → 同上,fail-safe
# 派生 (按 env):
#   DEPLOY_NAME=${APP_NAME}-${SLUG}[-int]  → FC functionName/子域名/s 项目名 (DNS-safe 连字符)
#   TABLE_PREFIX=${APP_NAME}_${SLUG}_[int_] → Supabase 表 (SQL-safe 下划线,见 remove-extension.sh)
if [ -z "${SLUG:-}" ]; then
  SLUG="$(openssl rand -hex 2)"
  printf '\nSLUG=%s\n' "$SLUG" >> "$EXT_DIR/ext.conf"
  echo "  📝 锁定 SLUG=$SLUG (含 4 位散列防同名冲突,已 bake 进 ext.conf)"
fi
BASE_NAME="${APP_NAME}-${SLUG}"
# 环境名 integration / prod 是人读的；进资源名的是短标签 ENV_TAG：int / （prod 为空，保持老地址不变）。
# 一眼看域名：带 -int 的就是测试环境。派生段与 publish.spec.ts 的 DERIVE_SNIPPET 同步。
if [ "$APP_ENV" = integration ]; then
  ENV_TAG=int;  DEPLOY_NAME="${BASE_NAME}-int"; TABLE_PREFIX="${APP_NAME}_${SLUG}_int_"
else
  ENV_TAG=prod; DEPLOY_NAME="${BASE_NAME}";     TABLE_PREFIX="${APP_NAME}_${SLUG}_"
fi
# 两个环境各自的渲染产物；s 用 name: 键状态，DEPLOY_NAME 不同即隔离。
S_FILE="s.${ENV_TAG}.yaml"
echo "  🎯 env=$APP_ENV → $DEPLOY_NAME (表前缀 $TABLE_PREFIX, 渲到 $S_FILE)"
# 渲出来的 s 文件含 supabase key，靠项目 .gitignore 挡在仓库外。模板已有 s.*.yaml；双环境之前 scaffold 的
# 存量项目只有 s.yaml 一行，s.int.yaml 会被当源码推上 GitHub——像 bake SLUG 一样幂等地补一行（改了
# .gitignore 下次 publish 会被「先推再发」拦一次，推上去的只是 .gitignore，无害）。
if ! grep -qxF 's.*.yaml' "$EXT_DIR/.gitignore" 2>/dev/null; then
  printf 's.*.yaml\n' >> "$EXT_DIR/.gitignore"
  echo "  📝 .gitignore 补 s.*.yaml (渲出的 s.int.yaml / s.prod.yaml 含 key，不进仓库；记得把 .gitignore 推上去)"
fi

# 2) 工具链 + 阿里云凭证（进程 env，本机云上同一条；见 ensure-credentials.sh）
source "$KIT_DIR/scripts/ensure-toolchain.sh"
ensure_toolchain
source "$KIT_DIR/scripts/ensure-credentials.sh"
ensure_credentials

# 3) persona hook（可选）：校验/导出 persona 特化运行时变量（如 SESSION_SECRET / APP_BASE_URL / persona 密钥）
if [ -f "$PERSONA_KIT/hooks/pre-deploy.sh" ]; then
  source "$PERSONA_KIT/hooks/pre-deploy.sh"
fi

# 4) build（框架相关）
echo "::stage::build"
if [ -n "$BUILD_CMD" ]; then
  echo "==> build: $BUILD_CMD"
  ( cd "$EXT_DIR" && eval "$BUILD_CMD" )
fi

# 5) bootstrap（共享，框架无关）
cp "$KIT_DIR/contract/bootstrap" "$EXT_DIR/$CODE_DIR/bootstrap"
chmod +x "$EXT_DIR/$CODE_DIR/bootstrap"

# 6) 通用占位符替换渲染 s.yaml：扫 persona s.yaml.tmpl 的所有 __X__，从 env 取同名变量
#（APP_ENV / TABLE_PREFIX 也走这条：模板里 __APP_ENV__ / __TABLE_PREFIX__ 渲进 FC 的 environmentVariables）
#
# 自定义域名段按履约拼接（M1b Step 5c）：APP_DOMAIN_SUFFIX 非空 = 部署级履约（域名与通配符
# 证书都在我们自己的阿里云账号上）→ 把 s.yaml.domain.tmpl 接上；空 = 使用者自带 key，
# 发布落在他自己的账号里，那里既没有我们的 DNS 也没有我们的证书 → 整段不渲染，用 FC 默认域名。
# 拼完再走同一套占位符替换，渲染契约本身不分叉。
TMPL="$PERSONA_KIT/contract/s.yaml.tmpl"
[ -f "$TMPL" ] || { echo "❌ 缺 $TMPL" >&2; exit 1; }
# **判据是「这个 persona 有没有 s.yaml.domain.tmpl」**，不是光看 APP_DOMAIN_SUFFIX：
# 没有那个文件的 persona（persona-dataclaw-builder）把自定义域名段写死在自己的 s.yaml.tmpl 里，
# 它压根不参与这套切换，替它选分支只会把它的发布路径改坏。
DOMAIN_TMPL="$PERSONA_KIT/contract/s.yaml.domain.tmpl"
TMPL_RENDER="$TMPL"
TMPL_COMBINED=""
if [ -f "$DOMAIN_TMPL" ]; then
  if [ -n "${APP_DOMAIN_SUFFIX:-}" ]; then
    # mktemp 落 EXT_DIR 之外：占位符缺失时下面直接 exit，别在用户项目目录里留残骸。
    # **必须写全 XXXXXX 模板**：BSD mktemp（macOS）的 `-t prefix` 会自己补随机后缀，
    # GNU coreutils（Linux，= M1b 的 Pod）要求模板以 ≥3 个 X 结尾，`mktemp -t s-yaml-tmpl`
    # 直接 `too few X's in template` 退 1（coreutils 9.1 实测），`set -e` 下当场打死这条
    # 默认路径。deploy-kit 的脚本两套 userland 都要跑，别用 BSD/GNU 语义有分歧的写法。
    TMPL_COMBINED="$(mktemp "${TMPDIR:-/tmp}/s-yaml-tmpl.XXXXXX")"
    trap 'rm -f "$TMPL_COMBINED"' EXIT
    cat "$TMPL" "$DOMAIN_TMPL" > "$TMPL_COMBINED"
    TMPL_RENDER="$TMPL_COMBINED"
    echo "  🌐 自定义域名段：${DEPLOY_NAME}.${APP_DOMAIN_SUFFIX}"
  else
    echo "  🌐 未配 APP_DOMAIN_SUFFIX（使用者自带 key 履约）：跳过自定义域名段，用 FC 默认域名"
  fi
fi
sed_args=()
for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL_RENDER" | sort -u); do
  var="${ph#__}"; var="${var%__}"
  # declare -p 测「是否定义」（含空字符串），兼容 bash 3.2；未定义即 fail-loud
  if ! declare -p "$var" >/dev/null 2>&1; then
    echo "❌ s.yaml.tmpl 占位符 $ph 无对应 env 变量 \$$var" >&2
    # 最常见的成因不是「漏配」，而是**这个脚本被手动跑了**：凭据由 daemon 在 spawn 时注进
    # 它起的那个子进程，别的地方起的 shell 拿不到。把这条写进报错里，是因为读它的下一个
    # 读者往往是 agent——不给方向它就会去翻凭据、手动 export 或写回 config.env，
    # 正好把「值不落 persona 目录」这条纪律拆掉。
    echo "   如果你是手动跑这个脚本：凭据只注给 daemon 起的那个进程，手动跑必然缺。" >&2
    echo "   正确做法是改调 clawd-app-builder 的 publish tool 重跑；**不要**去找 key 手动 export，也不要写回 config.env。" >&2
    echo "   确实是漏配（新增了占位符 / 换了实例）：改 daemon 的凭据表（~/.clawd/secrets/），不是改模板。" >&2
    exit 1
  fi
  # 转义 sed 替换串里的特殊字符（密钥/URL 可能含 \ | &），顺序：先 \ 再 | / &
  val="${!var}"
  val="${val//\\/\\\\}"
  val="${val//|/\\|}"
  val="${val//&/\\&}"
  sed_args+=(-e "s|$ph|$val|g")
done
sed "${sed_args[@]}" "$TMPL_RENDER" > "$EXT_DIR/$S_FILE"

# 7) deploy + verify
echo "::stage::deploy"
echo "==> s deploy ($DEPLOY_NAME, env=$APP_ENV)"
( cd "$EXT_DIR" && s deploy -y -t "$S_FILE" --access "$S_ACCESS_ALIAS" )

echo "::stage::verify"
# 产物 URL 怎么取，取决于**这个 persona 参不参与 5c 的域名段方案**（判据同上面渲染那步，
# 不许各判各的）：
#   有 s.yaml.domain.tmpl（app-builder）：
#     APP_DOMAIN_SUFFIX 非空 → 自定义域名，从 custom-domains 精确匹配
#     APP_DOMAIN_SUFFIX 为空 → 使用者自带 key，FC 默认域名，从 httpTrigger 取
#   没有（persona-dataclaw-builder 等）：自定义域名段写死在它自己的 s.yaml.tmpl 里，
#     **原样走老路**——它的 functionName 规则、域名策略都跟这套无关，别替它选分支。
#
# JSON 解析用 node 不用 python3：目标机器保证有 node（clawd 本身就是 node 跑的），不保证有
# python。而且原先 `2>/dev/null | python3 -c` 把 CLI 的 stderr 吞了再让 python 抛
# JSONDecodeError，`set -e` 下脚本直接崩在 traceback 上，真凶（凭证不对 / 超时）全丢——
# 下面的 fc_get 保留 CLI 原文并 fail-loud。
fc_get() {  # fc_get <path>：调 FC OpenAPI；失败把 CLI 原文写 stderr 后 return 1
  local out rc
  out="$(aliyun_ak fc GET "$1" 2>&1)" && rc=0 || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "❌ aliyun fc GET $1 失败，CLI 原文：" >&2
    echo "$out" >&2
    return 1
  fi
  printf '%s' "$out"
}
match_custom_domain() {  # match_custom_domain <expected>：命中返回该域名，否则空
  fc_get /2023-03-30/custom-domains | node -e '
const d = JSON.parse(require("fs").readFileSync(0, "utf8"));
const target = process.argv[1];
console.log((d.customDomains || []).some((c) => c.domainName === target) ? target : "");
' "$1"
}

PROD_URL=""
ALLOW_CD=""
if [ -f "$DOMAIN_TMPL" ] && [ -z "${APP_DOMAIN_SUFFIX:-}" ]; then
  # 单条 GetTrigger 而不是集合 ListTriggers：triggerName 由 persona 的 s.yaml.tmpl 契约固定
  # 为 httpTrigger（写死是确定的，不是猜的）。
  PROD_URL="$(fc_get "/2023-03-30/functions/${DEPLOY_NAME}/triggers/httpTrigger" | node -e '
const d = JSON.parse(require("fs").readFileSync(0, "utf8"));
console.log((d.httpTrigger && d.httpTrigger.urlInternet) || "");
')"
  MISS_HINT="未取到 FC 默认域名（函数 ${DEPLOY_NAME} 的 httpTrigger urlInternet 为空），检查 s deploy 是否真的建出了 httpTrigger"
  # FC 默认域名（*.fcapp.run）对所有响应强制注入 Content-Disposition: attachment，没有
  # 自定义域名就消不掉。使用者自带 key 履约的验收口径本来就是「公网可访问即可」
  # （design ③：per-user 部署用 FC 默认域名，不要求品牌域名），所以降级成警告不判失败。
  ALLOW_CD="--allow-content-disposition"
else
  EXPECTED_DOM="${DEPLOY_NAME}.${APP_DOMAIN_SUFFIX:-app.clawos.chat}"
  DOM="$(match_custom_domain "$EXPECTED_DOM")"
  [ -n "$DOM" ] && PROD_URL="https://$DOM"
  MISS_HINT="未取到自定义域名 $EXPECTED_DOM,检查 fc3-domain 是否成功 + DNS 泛解析 CNAME 是否生效"
fi

echo ""
echo "=================================================="
if [ -n "$PROD_URL" ]; then
  echo "::prod-url::$PROD_URL"
  echo "  ✅ 发布完成: $PROD_URL"
  echo "=================================================="
  bash "$KIT_DIR/scripts/verify.sh" "$PROD_URL" $ALLOW_CD
else
  echo "❌ 部署完成但$MISS_HINT" >&2
  exit 1
fi
