#!/usr/bin/env bash
# 内网生产：只换 api（presurvey-api:v1.0.6）—— 绝不碰 web/db
# ─────────────────────────────────────────────────────────────────────────────
# 在【内网生产服务器】上、以 root 跑：
#   su -c 'bash server-deploy-api-v1.0.6.sh'        # 或 sudo bash server-deploy-api-v1.0.6.sh
# 默认读当前目录的 tar，也可传路径：
#   bash server-deploy-api-v1.0.6.sh /path/to/presurvey-api-v1.0.6-20260604-183508-arm64.tar
#
# 硬约束（已写进逻辑，请勿改动）：
#   - 本脚本只操作 presurvey-api 容器：stop / rm / run 仅作用于 api。
#   - 绝不出现 web/db 容器的 stop/rm/run，绝不 truncate / docker compose down -v / 跑 init.sql。
#   - Docker 18.09 兼容：只用 docker load / inspect / run / ps，不用 compose、不用 --platform。
#   - 删除老 api 前先探测并沿用 network、端口、DB_* 等连接环境变量，以及 /app/geo_data 挂载。
#   - api 默认只在 Docker 网络内给 web 访问；只有老容器原本暴露宿主端口时才继续 -p。
#   - #48/#49 必需迁移：按序幂等应用 V2 reason CHECK + V3 site_delete_undo，不动数据。
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail

TAR=${1:-./presurvey-api-v1.0.6-20260604-183508-arm64.tar}
IMAGE=presurvey-api:v1.0.6
CTN=presurvey-api

# 探测失败时的回落默认；若现网容器存在，会优先沿用现网探测结果
DEFAULT_NET=presurvey-net

ENV_FILE=$(mktemp /tmp/presurvey-api-env.XXXXXX)
trap 'rm -f "$ENV_FILE"' EXIT

echo "== 0. 检查 tar 文件存在 =="
[ -f "$TAR" ] || { echo "!! tar 不存在：$TAR"; echo "   先拉取：curl -L -O https://img.mangosv5.app/presurvey-api-v1.0.6-20260604-183508-arm64.tar"; exit 1; }
echo "   tar = $TAR"

echo "== 1. 探测现网老 $CTN 的 network / 端口 / DB环境变量 / geo_data挂载（删除前，删了就探不到了）=="
NET=""
HOSTPORT=""
GEO_MOUNT=""
if docker inspect "$CTN" >/dev/null 2>&1; then
    NET=$(docker inspect -f '{{.HostConfig.NetworkMode}}' "$CTN" 2>/dev/null || true)
    HOSTPORT=$(docker inspect -f '{{(index (index .NetworkSettings.Ports "8000/tcp") 0).HostPort}}' "$CTN" 2>/dev/null || true)
    docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$CTN" \
        | grep -E '^(DB_|DATABASE_|POSTGRES_|PRESURVEY_)' > "$ENV_FILE" || true
    GEO_MOUNT=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/app/geo_data"}}{{.Source}}:{{.Destination}}:{{.Mode}}{{end}}{{end}}' "$CTN" 2>/dev/null || true)
    echo "   探测到：network=$NET  hostport=$HOSTPORT"
    if [ -s "$ENV_FILE" ]; then
        echo "   探测到 DB/应用环境变量："
        sed 's/=.*/=<hidden>/' "$ENV_FILE" | sed 's/^/     /'
    else
        echo "   ⚠️  未探测到 DB_* / DATABASE_* / POSTGRES_* / PRESURVEY_* 环境变量。"
    fi
    if [ -n "$GEO_MOUNT" ]; then
        echo "   探测到 geo_data 挂载：$GEO_MOUNT"
    else
        echo "   未探测到 /app/geo_data 挂载；将不额外挂载。"
    fi
else
    echo "   ⚠️  老 $CTN 容器不存在，无法探测。会使用回落默认，人工务必核对 DB 连接。"
fi

if [ -z "$NET" ] || [ "$NET" = "default" ] || [ "$NET" = "null" ]; then
    echo "   ⚠️  network 探测无效（值=[$NET]），回落默认：$DEFAULT_NET"
    NET=$DEFAULT_NET
fi
if [ -z "$HOSTPORT" ] || [ "$HOSTPORT" = "null" ]; then
    HOSTPORT=""
    echo "   未探测到宿主端口映射；将保持纯 Docker 网络内访问，不强制 -p，避免占用生产宿主 8000。"
fi
if [ -n "$HOSTPORT" ]; then
    echo "   ▶ 最终采用：network=$NET  端口映射=${HOSTPORT}:8000"
else
    echo "   ▶ 最终采用：network=$NET  端口映射=<none>，web 通过 network-alias api 访问"
fi

echo "== 2. docker load 新镜像 =="
docker load -i "$TAR"

echo "== 3. 校验 $IMAGE 已 load =="
docker images | grep presurvey-api | grep -q "1.0.6" \
    || { echo "!! 未找到 presurvey-api v1.0.6 镜像，load 失败，停。"; docker images | grep presurvey-api || true; exit 1; }
echo "   OK，镜像就绪。"

echo "== 4. #48/#49 幂等迁移 V2 + V3（停删老 api 前执行，不碰数据卷）=="
# 兼容目标机仍停在 v1.0.4 的情况：先放宽 reason CHECK(V2)，再建删除撤销表/索引(V3)。
# 这里复用新 api 镜像内的 asyncpg，在同一 Docker 网络里按老 api 的 DB_* 环境变量连接数据库，只做幂等 DDL，不改业务数据。
MIGRATE_ARGS=(--rm --network "$NET")
if [ -s "$ENV_FILE" ]; then
    MIGRATE_ARGS+=(--env-file "$ENV_FILE")
else
    echo "   ⚠️  无 DB_* 环境变量，迁移将使用镜像/脚本默认值：DB_HOST=db DB_PORT=5432 DB_USER=presurvey DB_PASSWORD=postgres DB_NAME=presurvey"
    MIGRATE_ARGS+=(-e DB_HOST=db -e DB_PORT=5432 -e DB_USER=presurvey -e DB_PASSWORD=postgres -e DB_NAME=presurvey)
fi
docker run "${MIGRATE_ARGS[@]}" "$IMAGE" python - <<'PY'
import asyncio
import os
import asyncpg

V2_SQL = """
ALTER TABLE restore_point DROP CONSTRAINT IF EXISTS restore_point_reason_check;
ALTER TABLE restore_point ADD CONSTRAINT restore_point_reason_check
    CHECK (reason IN ('pre_import','pre_clear','pre_rollback','manual','auto_backup','pre_migrate','pre_feature_delete'));
"""

V3_SQL = """
CREATE SEQUENCE IF NOT EXISTS site_delete_undo_batch_seq;

CREATE TABLE IF NOT EXISTS site_delete_undo (
    undo_id      BIGINT      NOT NULL,
    deleted_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    undone       BOOLEAN     NOT NULL DEFAULT false,
    site_id      TEXT        NOT NULL,
    "option"     TEXT        NOT NULL DEFAULT '',
    project      TEXT,
    site_status  TEXT,
    operator     TEXT,
    category     TEXT,
    type         TEXT,
    lati         DOUBLE PRECISION,
    longi        DOUBLE PRECISION,
    extras       JSONB       NOT NULL DEFAULT '{}'::jsonb,
    source_file  TEXT,
    created_at   TIMESTAMPTZ,
    updated_at   TIMESTAMPTZ,
    geom         GEOMETRY(Point, 4326)
);

CREATE INDEX IF NOT EXISTS site_delete_undo_batch_idx ON site_delete_undo (undo_id);
CREATE INDEX IF NOT EXISTS site_delete_undo_deleted_at_idx ON site_delete_undo (deleted_at DESC);
"""

async def main():
    conn = await asyncpg.connect(
        host=os.getenv("DB_HOST", "db"),
        port=int(os.getenv("DB_PORT", "5432")),
        user=os.getenv("DB_USER", "presurvey"),
        password=os.getenv("DB_PASSWORD", "postgres"),
        database=os.getenv("DB_NAME", "presurvey"),
        timeout=15,
    )
    try:
        await conn.execute(V2_SQL)
        row = await conn.fetchrow(
            """
            SELECT pg_get_constraintdef(oid) AS def
            FROM pg_constraint
            WHERE conrelid='restore_point'::regclass
              AND conname='restore_point_reason_check'
            """
        )
        definition = row["def"] if row else ""
        print(definition)
        if "pre_feature_delete" not in definition:
            raise SystemExit("restore_point_reason_check still lacks pre_feature_delete")

        await conn.execute(V3_SQL)
        cols = await conn.fetch(
            """
            SELECT column_name
            FROM information_schema.columns
            WHERE table_schema='public'
              AND table_name='site_delete_undo'
              AND column_name IN ('undo_id','undone')
            """
        )
        found = {r["column_name"] for r in cols}
        print("site_delete_undo columns=" + ",".join(sorted(found)))
        if found != {"undo_id", "undone"}:
            raise SystemExit("site_delete_undo lacks required columns undo_id/undone")
    finally:
        await conn.close()

asyncio.run(main())
PY
echo "   OK，V2 reason CHECK 与 V3 site_delete_undo 均已就绪。"

echo "== 5. 停删老 api（幂等，只动 api，不碰 web/db）=="
docker stop "$CTN" 2>/dev/null || true
docker rm   "$CTN" 2>/dev/null || true

echo "== 6. 起新 api（Docker 18.09 docker run，沿用探测到的 network/端口/环境/挂载）=="
# web/nginx.conf 反代到 http://api:8000，生产依赖 api 这个 Docker 网络别名。
# api 健康验证也走 Docker 网络内的 api:8000，因此没有老宿主端口映射时不加 -p，避免端口冲突导致换容器失败。
RUN_ARGS=(-d --name "$CTN" --network "$NET" --network-alias api)
if [ -n "$HOSTPORT" ]; then
    RUN_ARGS+=(-p "${HOSTPORT}:8000")
fi
if [ -s "$ENV_FILE" ]; then
    RUN_ARGS+=(--env-file "$ENV_FILE")
fi
if [ -n "$GEO_MOUNT" ] && [ "$GEO_MOUNT" != "::" ]; then
    RUN_ARGS+=(-v "$GEO_MOUNT")
fi

if ! docker run "${RUN_ARGS[@]}" "$IMAGE"; then
    echo "!! docker run 失败。供人工核对的探测值："
    echo "     network   = $NET"
    echo "     端口      = ${HOSTPORT:+${HOSTPORT}:8000}${HOSTPORT:-<none>}"
    echo "     alias     = api"
    echo "     geo_data  = ${GEO_MOUNT:-<none>}"
    echo "     env file  = $ENV_FILE（脚本退出后会删除；请重跑脚本重新探测）"
    echo "     镜像      = $IMAGE"
    echo "   排查：端口被占用？network 不存在？DB_* 是否缺失？docker network ls / docker ps -a / docker logs $CTN 看一下。"
    echo "   兜底：若 api 日志出现 \"can't start new thread\" 或 getaddrinfo 相关错误，重跑时追加启动参数覆盖 CMD：uvicorn main:app --host 0.0.0.0 --port 8000 --loop asyncio"
    exit 1
fi

echo "== 7. 验证 =="
sleep 3
if docker ps | grep -q "$CTN"; then
    echo "   容器运行中：$(docker ps --filter name=$CTN --format '{{.Names}}  {{.Status}}  {{.Ports}}')"
else
    echo "!! 容器未在运行，看日志：docker logs $CTN"
    docker ps -a | grep "$CTN" || true
    docker logs "$CTN" 2>/dev/null || true
    exit 1
fi

HEALTH_CHECKER=""
if docker images | grep -E '^curlimages/curl[[:space:]]' >/dev/null 2>&1; then
    HEALTH_CHECKER="curlimages/curl"
elif docker images | grep -E '^busybox[[:space:]]' >/dev/null 2>&1; then
    HEALTH_CHECKER="busybox"
fi

if [ "$HEALTH_CHECKER" = "curlimages/curl" ]; then
    docker run --rm --network "$NET" curlimages/curl -s -w '\n%{http_code}' http://api:8000/health > /tmp/presurvey-api-health.raw 2>/tmp/presurvey-api-health.err \
        && CODE=$(tail -n 1 /tmp/presurvey-api-health.raw) || CODE=000
    sed '$d' /tmp/presurvey-api-health.raw > /tmp/presurvey-api-health.out 2>/dev/null || true
elif [ "$HEALTH_CHECKER" = "busybox" ]; then
    docker run --rm --network "$NET" busybox wget -qO- http://api:8000/health > /tmp/presurvey-api-health.out 2>/tmp/presurvey-api-health.err \
        && CODE=200 || CODE=000
else
    CODE=$(docker exec "$CTN" python -c 'import urllib.request; r=urllib.request.urlopen("http://api:8000/health", timeout=5); print(r.status); print(r.read().decode())' > /tmp/presurvey-api-health.pyout 2>/tmp/presurvey-api-health.err \
        && sed -n '1p' /tmp/presurvey-api-health.pyout || echo "000")
    sed -n '2,$p' /tmp/presurvey-api-health.pyout > /tmp/presurvey-api-health.out 2>/dev/null || true
fi
echo "   Docker 网络内访问 http://api:8000/health  ->  HTTP $CODE"
cat /tmp/presurvey-api-health.out 2>/dev/null || true
echo ""
case "$CODE" in
    200)
        if [ -n "$HOSTPORT" ]; then
            echo "✅ api 已换到 v1.0.6 并就绪（network=$NET  alias=api  端口=${HOSTPORT}:8000）。web/db 未触碰。"
        else
            echo "✅ api 已换到 v1.0.6 并就绪（network=$NET  alias=api  无宿主端口映射）。web/db 未触碰。"
        fi
        ;;
    *)
        echo "⚠️  HTTP $CODE 非预期。看日志：docker logs $CTN"
        echo "   老容器探测值供人工核对：network=$NET  端口=${HOSTPORT:+${HOSTPORT}:8000}${HOSTPORT:-<none>}  geo_data=${GEO_MOUNT:-<none>}"
        echo "   兜底：若 api 日志出现 \"can't start new thread\" 或 getaddrinfo 相关错误，重跑时追加启动参数覆盖 CMD：uvicorn main:app --host 0.0.0.0 --port 8000 --loop asyncio"
        exit 1
        ;;
esac
