#!/usr/bin/env bash
# internal-app-platform — 单 app 容器部署脚本
#
# 由后端 DeployRunnerService 或 webhook handler 调用：
#   bash deploy-container.sh \
#     --employee-slug lijian \
#     --app-slug hello \
#     --repo-url http://43.130.59.228/FFAIApps/lijian-hello.git \
#     --runtime node \
#     --branch main \
#     [--app-id <uuid>] \
#     [--git-token <token>]      # 私仓 clone 用，可选；也支持 env GITEA_DEPLOY_TOKEN
#     [--skip-clone]             # 跳过 git clone（用于本地预先放好 repo 的测试场景）
#
# 输出（stdout）：JSON 结果，最后一行：
#   {"ok":true,"containerName":"...","internalHost":"...","externalUrl":"...","logsTail":[...]}
# 或 {"ok":false,"error":{"code":"...","message":"...","details":{...}}}
#
# 退出码：0 成功；非 0 失败（同时 stdout 输出错误 JSON）
#
# 幂等：同 (employee-slug, app-slug) 重复部署会替换旧容器（stop + rm + run）。

set -uo pipefail

# ---- 配置 ----
APPS_DOMAIN="${INTERNAL_APP_PUBLIC_DOMAIN:-apps.ffworkspace.faradayfuture.com}"
REPO_ROOT="${INTERNAL_APP_REPO_ROOT:-/srv/internal-apps/repos}"
DATA_ROOT="${INTERNAL_APP_DATA_ROOT:-/srv/internal-apps/data}"
CADDY_SITES_DIR="${INTERNAL_APP_CADDY_SITES_DIR:-/srv/caddy/sites}"
CADDY_CONTAINER="${INTERNAL_APP_CADDY_CONTAINER:-ffoa-caddy}"
DOCKER_NETWORK="${INTERNAL_APP_DOCKER_NETWORK:-ffoa-network}"
APP_INTERNAL_PORT=3000                     # 容器内固定监听端口
MEM_LIMIT="${INTERNAL_APP_MEM_LIMIT:-512m}"
CPU_LIMIT="${INTERNAL_APP_CPU_LIMIT:-0.5}"
NODE_IMAGE="${INTERNAL_APP_NODE_IMAGE:-node:20-alpine}"
STATIC_IMAGE="${INTERNAL_APP_STATIC_IMAGE:-caddy:2-alpine}"
HEALTH_TIMEOUT_S="${INTERNAL_APP_HEALTH_TIMEOUT_S:-30}"
# Litestream backup（可选，set 即启用 sidecar）
LITESTREAM_ENABLED="${INTERNAL_APP_LITESTREAM_ENABLED:-true}"
LITESTREAM_IMAGE="${INTERNAL_APP_LITESTREAM_IMAGE:-litestream/litestream:0.3}"
MINIO_CONTAINER="${INTERNAL_APP_MINIO_CONTAINER:-ffoa-internal-apps-minio}"
MINIO_BUCKET="${INTERNAL_APP_MINIO_BUCKET:-internal-apps-backups}"
# MinIO 凭据优先级：caller env > /srv/internal-apps/.env.minio 文件兜底
# （脚本被 ssh 或 cron 调用时 caller 可能没 export 这些 env）
MINIO_ENV_FILE="${INTERNAL_APP_MINIO_ENV_FILE:-/srv/internal-apps/.env.minio}"
if [[ -z "${MINIO_ROOT_USER:-}" && -r "$MINIO_ENV_FILE" ]]; then
  # shellcheck disable=SC1090
  set -a; . "$MINIO_ENV_FILE"; set +a
fi
MINIO_ACCESS_KEY="${INTERNAL_APP_MINIO_ROOT_USER:-${MINIO_ROOT_USER:-}}"
MINIO_SECRET_KEY="${INTERNAL_APP_MINIO_ROOT_PASSWORD:-${MINIO_ROOT_PASSWORD:-}}"

# ---- 解析 args ----
EMPLOYEE_SLUG=""
APP_SLUG=""
REPO_URL=""
RUNTIME=""
BRANCH="main"
APP_ID=""
GIT_TOKEN="${GITEA_DEPLOY_TOKEN:-}"
SKIP_CLONE=0
APPS_DOMAIN_FLAG=""  # 显式 flag 覆盖 env，让调用方（backend ConfigService）作为权威

while [[ $# -gt 0 ]]; do
  case "$1" in
    --employee-slug) EMPLOYEE_SLUG="$2"; shift 2 ;;
    --app-slug)      APP_SLUG="$2"; shift 2 ;;
    --repo-url)      REPO_URL="$2"; shift 2 ;;
    --runtime)       RUNTIME="$2"; shift 2 ;;
    --branch)        BRANCH="$2"; shift 2 ;;
    --app-id)        APP_ID="$2"; shift 2 ;;
    --git-token)     GIT_TOKEN="$2"; shift 2 ;;
    --apps-domain)   APPS_DOMAIN_FLAG="$2"; shift 2 ;;
    --skip-clone)    SKIP_CLONE=1; shift ;;
    *) echo "unknown arg: $1" >&2; exit 64 ;;
  esac
done

# flag 优先于 env（NestJS ConfigService 不写 process.env，子进程拿不到 env）
if [[ -n "$APPS_DOMAIN_FLAG" ]]; then
  APPS_DOMAIN="$APPS_DOMAIN_FLAG"
fi

# 结构化错误输出 helper
emit_err() {
  local code="$1" msg="$2" details="${3:-{\}}"
  printf '{"ok":false,"error":{"code":"%s","message":"%s","details":%s}}\n' "$code" "$msg" "$details"
  exit 1
}

emit_ok() {
  local container="$1" internal_host="$2" external_url="$3" logs_tail_json="$4"
  printf '{"ok":true,"containerName":"%s","internalHost":"%s","externalUrl":"%s","logsTail":%s}\n' \
    "$container" "$internal_host" "$external_url" "$logs_tail_json"
}

# ---- 入参校验 ----
[[ -z "$EMPLOYEE_SLUG" ]] && emit_err "invalid_args" "--employee-slug is required"
[[ -z "$APP_SLUG" ]]      && emit_err "invalid_args" "--app-slug is required"
[[ -z "$RUNTIME" ]]       && emit_err "invalid_args" "--runtime is required (node|static)"
[[ "$RUNTIME" != "node" && "$RUNTIME" != "static" ]] && \
  emit_err "invalid_args" "--runtime must be node or static, got: $RUNTIME"
if [[ $SKIP_CLONE -eq 0 && -z "$REPO_URL" ]]; then
  emit_err "invalid_args" "--repo-url is required unless --skip-clone"
fi

# ---- 命名约定 ----
CONTAINER_NAME="ffoa-app-${EMPLOYEE_SLUG}-${APP_SLUG}"
INTERNAL_HOST="${CONTAINER_NAME}"          # docker 内部 DNS
EXTERNAL_HOST="${EMPLOYEE_SLUG}-${APP_SLUG}.${APPS_DOMAIN}"
EXTERNAL_URL="https://${EXTERNAL_HOST}"
REPO_DIR="${REPO_ROOT}/${EMPLOYEE_SLUG}/${APP_SLUG}"
DATA_DIR="${DATA_ROOT}/${EMPLOYEE_SLUG}/${APP_SLUG}"      # 持久化 SQLite / Litestream 共享
LITESTREAM_CONTAINER="${CONTAINER_NAME}-bk"
CADDY_SITE_FILE="${CADDY_SITES_DIR}/${EMPLOYEE_SLUG}-${APP_SLUG}.caddy"

# 容器名 docker 最长 63 字符
if [[ ${#CONTAINER_NAME} -gt 63 ]]; then
  emit_err "name_too_long" "container name length ${#CONTAINER_NAME} > 63" \
    "{\"containerName\":\"$CONTAINER_NAME\"}"
fi

# ---- 1. clone / pull ----
mkdir -p "$(dirname "$REPO_DIR")"

if [[ $SKIP_CLONE -eq 0 ]]; then
  # 注入 token（如有）到 URL：http://user:token@host/path
  AUTH_URL="$REPO_URL"
  if [[ -n "$GIT_TOKEN" && "$REPO_URL" == http* ]]; then
    AUTH_URL=$(printf '%s' "$REPO_URL" | sed -E "s#^(https?://)#\\1oauth2:${GIT_TOKEN}@#")
  fi

  if [[ -d "$REPO_DIR/.git" ]]; then
    git -C "$REPO_DIR" remote set-url origin "$AUTH_URL" >/dev/null 2>&1 || true
    if ! git -C "$REPO_DIR" fetch origin "$BRANCH" 2>/tmp/git-fetch.err; then
      emit_err "git_fetch_failed" "git fetch origin $BRANCH failed" \
        "{\"stderr\":\"$(jq -Rs . </tmp/git-fetch.err | head -c 400)\"}"
    fi
    git -C "$REPO_DIR" checkout -B "$BRANCH" "origin/$BRANCH" >/dev/null 2>&1 || \
      emit_err "git_checkout_failed" "checkout $BRANCH failed"
  else
    if ! git clone --depth 50 --branch "$BRANCH" "$AUTH_URL" "$REPO_DIR" 2>/tmp/git-clone.err; then
      local_err=$(head -c 400 /tmp/git-clone.err | tr -d '"' | tr '\n' ' ')
      emit_err "git_clone_failed" "git clone failed" \
        "{\"stderr\":\"$local_err\"}"
    fi
  fi
fi

[[ ! -d "$REPO_DIR" ]] && emit_err "repo_missing" "repo dir $REPO_DIR not found"

# ---- 2. 校验 runtime 与文件结构匹配 ----
case "$RUNTIME" in
  node)
    [[ ! -f "$REPO_DIR/package.json" ]] && \
      emit_err "no_package_json" "runtime=node 但 $REPO_DIR/package.json 不存在"
    if ! jq -e '.scripts.start' "$REPO_DIR/package.json" >/dev/null 2>&1; then
      emit_err "no_start_script" "package.json scripts.start 缺失"
    fi
    ;;
  static)
    [[ ! -f "$REPO_DIR/index.html" ]] && \
      emit_err "no_index_html" "runtime=static 但 $REPO_DIR/index.html 不存在"
    ;;
esac

# ---- 3. 停旧容器 ----
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true

# ---- 4. 持久化数据目录 + 可选 restore ----
mkdir -p "$DATA_DIR"
# 如果 MinIO 上有该 app 的备份且本地 app.db 不存在（新机器/恢复场景），先拉回。
# 注：不能用 "DATA_DIR 整体空"判定——.litestream.yml 等元文件可能存在但 app.db 已丢
if [[ "$LITESTREAM_ENABLED" == "true" && ! -f "$DATA_DIR/app.db" && -n "$MINIO_ACCESS_KEY" ]]; then
  # 用 inline YAML 配置（query-string 参数在 0.3 不传到 bucket-region 探测，会失败）
  TMP_CFG=$(mktemp)
  cat > "$TMP_CFG" <<LSCFG
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: ${MINIO_BUCKET}
        path: ${EMPLOYEE_SLUG}/${APP_SLUG}/app.db
        endpoint: http://${MINIO_CONTAINER}:9000
        region: us-east-1
        force-path-style: true
        access-key-id: ${MINIO_ACCESS_KEY}
        secret-access-key: ${MINIO_SECRET_KEY}
LSCFG
  docker run --rm \
    --network "$DOCKER_NETWORK" \
    -v "$DATA_DIR":/data \
    -v "$TMP_CFG":/etc/litestream.yml:ro \
    "$LITESTREAM_IMAGE" \
    restore -config /etc/litestream.yml -if-replica-exists -o /data/app.db /data/app.db \
    >/dev/null 2>/dev/null || true
  rm -f "$TMP_CFG"
fi

# ---- 5. 起新容器 ----
COMMON_DOCKER_ARGS=(
  --name "$CONTAINER_NAME"
  --network "$DOCKER_NETWORK"
  --restart unless-stopped
  --memory "$MEM_LIMIT"
  --cpus "$CPU_LIMIT"
  --label "ffoa.platform=internal-apps"
  --label "ffoa.employee=${EMPLOYEE_SLUG}"
  --label "ffoa.app=${APP_SLUG}"
)
[[ -n "$APP_ID" ]] && COMMON_DOCKER_ARGS+=(--label "ffoa.app_id=${APP_ID}")

if [[ "$RUNTIME" == "node" ]]; then
  # /data 持久化挂载（SQLite 等数据写这里），Litestream sidecar 共享同一目录
  docker run -d \
    "${COMMON_DOCKER_ARGS[@]}" \
    -e PORT="$APP_INTERNAL_PORT" \
    -e NODE_ENV=production \
    -e DATA_DIR=/data \
    -v "$REPO_DIR":/app:ro \
    -v "$DATA_DIR":/data \
    -w /app \
    "$NODE_IMAGE" \
    sh -c "cp -r /app /tmp/app && cd /tmp/app && (npm ci --omit=dev --no-audit --no-fund 2>&1 || npm install --omit=dev --no-audit --no-fund) && exec npm start" \
    >/dev/null 2>/tmp/docker-run.err || \
    emit_err "docker_run_failed" "docker run failed" \
      "{\"stderr\":\"$(tr -d '"' </tmp/docker-run.err | tr '\n' ' ' | head -c 400)\"}"
else
  # static: caddy 容器跑 file-server（无 /data，不需要持久化）
  docker run -d \
    "${COMMON_DOCKER_ARGS[@]}" \
    -v "$REPO_DIR":/srv:ro \
    "$STATIC_IMAGE" \
    caddy file-server --root /srv --listen :"$APP_INTERNAL_PORT" \
    >/dev/null 2>/tmp/docker-run.err || \
    emit_err "docker_run_failed" "docker run failed" \
      "{\"stderr\":\"$(tr -d '"' </tmp/docker-run.err | tr '\n' ' ' | head -c 400)\"}"
fi

# ---- 5. 等容器内端口监听 ----
deadline=$(( $(date +%s) + HEALTH_TIMEOUT_S ))
ready=0
while [[ $(date +%s) -lt $deadline ]]; do
  state=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo missing)
  if [[ "$state" != "running" ]]; then
    sleep 1
    continue
  fi
  # 用 caddy 容器作探针（避免在 app 容器里假设 curl 存在）
  if docker exec "$CADDY_CONTAINER" wget -q -T 2 -O /dev/null \
       "http://${INTERNAL_HOST}:${APP_INTERNAL_PORT}/" 2>/dev/null; then
    ready=1
    break
  fi
  sleep 2
done

if [[ $ready -ne 1 ]]; then
  logs_snip=$(docker logs --tail 30 "$CONTAINER_NAME" 2>&1 | tr -d '"' | tr '\n' ' ' | head -c 800)
  emit_err "health_check_timeout" \
    "容器 $HEALTH_TIMEOUT_S 秒内未在 :${APP_INTERNAL_PORT} 监听" \
    "{\"containerName\":\"$CONTAINER_NAME\",\"logs\":\"$logs_snip\"}"
fi

# ---- 6. 写 Caddy site 配置 + reload ----
# Phase 0：DNS 还没上线，强制 http:// 防止 Caddy auto-HTTPS 触发 ACME 失败（NXDOMAIN）。
# Phase 1 共机模式（docs/modules/internal-app-platform/12-nginx-caddy-coexist.md）：
#   保留 http:// 前缀——TLS 由前置 nginx 终止，Caddy 仅在内网处理 HTTP，不应触发 ACME。
#   独立模式（dev 沙箱）DNS 上线后才考虑去掉 http:// 让 Caddy 自签 LE。
CADDY_SCHEME="${INTERNAL_APP_CADDY_SCHEME:-http://}"
cat > "$CADDY_SITE_FILE" <<CADDY_SITE
${CADDY_SCHEME}${EXTERNAL_HOST} {
    reverse_proxy ${INTERNAL_HOST}:${APP_INTERNAL_PORT}
    encode gzip
    log {
        output stdout
        format json
    }
}
CADDY_SITE

if ! docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile 2>/tmp/caddy-reload.err; then
  rm -f "$CADDY_SITE_FILE"   # 回滚配置避免 Caddy 卡半状态
  emit_err "caddy_reload_failed" "caddy reload failed (config rolled back)" \
    "{\"stderr\":\"$(tr -d '"' </tmp/caddy-reload.err | tr '\n' ' ' | head -c 400)\"}"
fi

# ---- 7. Litestream sidecar（只 node runtime；static 无 /data 不需要）----
# 仅当 LITESTREAM_ENABLED=true + MinIO credentials 都存在时启动。
# sidecar 跟 main 容器共享 $DATA_DIR；监控 /data/app.db 增量复制到 MinIO。
# 失败仅 warn 进 stderr，不中断主部署（备份是 nice-to-have，缺失不影响访问）。
#
# 注：Litestream 0.3 + MinIO 必须走 YAML config 文件（query-string 参数在
# bucket-region 探测时不传递，会 InvalidAccessKeyId）；详见
# .learnings/2026-05-14-litestream-yaml-config-needed-for-minio.md
if [[ "$RUNTIME" == "node" && "$LITESTREAM_ENABLED" == "true" \
      && -n "$MINIO_ACCESS_KEY" && -n "$MINIO_SECRET_KEY" ]]; then
  docker rm -f "$LITESTREAM_CONTAINER" >/dev/null 2>&1 || true

  # 生成 per-app litestream config（明文密码，0600 权限）
  LITESTREAM_CONFIG_FILE="${DATA_DIR}/.litestream.yml"
  cat > "$LITESTREAM_CONFIG_FILE" <<LITESTREAM_CFG
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: ${MINIO_BUCKET}
        path: ${EMPLOYEE_SLUG}/${APP_SLUG}/app.db
        endpoint: http://${MINIO_CONTAINER}:9000
        region: us-east-1
        force-path-style: true
        access-key-id: ${MINIO_ACCESS_KEY}
        secret-access-key: ${MINIO_SECRET_KEY}
LITESTREAM_CFG
  chmod 0600 "$LITESTREAM_CONFIG_FILE"

  docker run -d \
    --name "$LITESTREAM_CONTAINER" \
    --network "$DOCKER_NETWORK" \
    --restart unless-stopped \
    --label "ffoa.platform=internal-apps-backup" \
    --label "ffoa.employee=${EMPLOYEE_SLUG}" \
    --label "ffoa.app=${APP_SLUG}" \
    -v "$DATA_DIR":/data \
    -v "$LITESTREAM_CONFIG_FILE":/etc/litestream.yml:ro \
    "$LITESTREAM_IMAGE" \
    replicate -config /etc/litestream.yml \
    >/dev/null 2>/tmp/litestream-run.err || \
    >&2 echo "[deploy] litestream sidecar 启动失败（不阻塞主部署）: $(head -c 200 /tmp/litestream-run.err)"
fi

# ---- 8. 收尾 ----
# 取最近 5 行容器日志做返回（前端展示给员工 "你的 app 起来了" 时用）
# jq -c 强制 compact 单行，避免 backend parseDeployOutput 只看最后一行解析 JSON 失败
logs_json=$(docker logs --tail 5 "$CONTAINER_NAME" 2>&1 | jq -R . | jq -sc .)

emit_ok "$CONTAINER_NAME" "${INTERNAL_HOST}:${APP_INTERNAL_PORT}" "$EXTERNAL_URL" "$logs_json"
