#!/usr/bin/env bash
# Usage: publish.sh <extension-directory> <persona-root> <integration|prod>
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"

[ -f "$PERSONA_KIT/config.env" ] && source "$PERSONA_KIT/config.env"
[ -f "$EXT_DIR/ext.conf" ] && source "$EXT_DIR/ext.conf"
# Set env after sourcing config: only the positional argument chooses the deployment environment.
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:-}"

# Persist a missing slug before deriving stable deployment and table names.
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}"
# Keep resource-name derivation aligned with 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_FILE="s.${ENV_TAG}.yaml"
echo "  🎯 env=$APP_ENV → $DEPLOY_NAME (表前缀 $TABLE_PREFIX, 渲到 $S_FILE)"
# Generated YAML contains secrets; add the wildcard ignore rule for older projects too.
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

source "$KIT_DIR/scripts/ensure-toolchain.sh"
ensure_toolchain
source "$KIT_DIR/scripts/ensure-credentials.sh"
ensure_credentials

if [ -f "$PERSONA_KIT/hooks/pre-deploy.sh" ]; then
  source "$PERSONA_KIT/hooks/pre-deploy.sh"
fi

echo "::stage::build"
if [ -n "$BUILD_CMD" ]; then
  echo "==> build: $BUILD_CMD"
  ( cd "$EXT_DIR" && eval "$BUILD_CMD" )
fi

cp "$KIT_DIR/contract/bootstrap" "$EXT_DIR/$CODE_DIR/bootstrap"
chmod +x "$EXT_DIR/$CODE_DIR/bootstrap"

TMPL="$PERSONA_KIT/contract/s.yaml.tmpl"
[ -f "$TMPL" ] || { echo "❌ 缺 $TMPL" >&2; exit 1; }
# Only personas with a separate domain template participate in domain selection; others keep their own template policy.
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
    # Keep temporary templates outside the project and use XXXXXX for both BSD and GNU mktemp.
    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 distinguishes missing from empty variables on Bash 3.2.
  if ! declare -p "$var" >/dev/null 2>&1; then
    echo "❌ s.yaml.tmpl 占位符 $ph 无对应 env 变量 \$$var" >&2
    # Missing injected credentials must be fixed through the daemon credential table, never written into templates.
    echo "   如果你是手动跑这个脚本：凭据只注给 daemon 起的那个进程，手动跑必然缺。" >&2
    echo "   正确做法是改调 clawd-app-builder 的 publish tool 重跑；**不要**去找 key 手动 export，也不要写回 config.env。" >&2
    echo "   确实是漏配（新增了占位符 / 换了实例）：改 daemon 的凭据表（~/.clawd/secrets/），不是改模板。" >&2
    exit 1
  fi
  # Escape backslashes before the other sed replacement characters.
  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"

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 discovery must use the same domain-template predicate as rendering.
# Preserve CLI stderr before parsing JSON with the guaranteed Node runtime.
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
  # The template contract fixes the trigger name to 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 default-domain Content-Disposition is tolerated only on the no-custom-domain path.
  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
