#!/usr/bin/env bash
# ============================================================
# 通用一键发布 extension 到阿里云 FC（persona 无关）
# 用法: publish.sh <extension目录> <persona根目录>
#   - <persona根目录>: daemon 按 session persona 解析后传入（.../personas/<id>）
#   - 平台凭证/脚本骨架/bootstrap 来自本脚本所在的共享 deploy-kit
#   - persona 注入值/占位符/特化逻辑/persona 密钥来自 <persona根>/extension-kit
# ============================================================
set -euo pipefail

KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"            # 共享 deploy-kit 根
EXT_DIR="${1:?用法: publish.sh <extension目录> <persona根目录>}"
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"
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
# 派生:
#   DEPLOY_NAME=${APP_NAME}-${SLUG}  → FC functionName/子域名/s 项目名 (DNS-safe 连字符)
#   表前缀=${APP_NAME}_${SLUG}_       → 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
DEPLOY_NAME="${APP_NAME}-${SLUG}"

# 2) 工具链 + 阿里云凭证（分层：shared demo + 可选 local override）
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 取同名变量
#
# 自定义域名段按履约拼接（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 "   正确做法是改调 appBuilder:publish RPC 重跑；**不要**去找 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.yaml"

# 7) deploy + verify
echo "::stage::deploy"
echo "==> s deploy ($DEPLOY_NAME)"
( cd "$EXT_DIR" && s deploy -y --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
