#!/usr/bin/env bash
set -euo pipefail

root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$root"

components=auto
production_host=${AI_GATEWAY_PRODUCTION_HOST:-root@110.42.51.33}
deploy_mode=${AI_GATEWAY_DEPLOY_MODE:-compose}
case $deploy_mode in
  compose) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-release ;;
  kubernetes) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-cluster-release ;;
  *) echo 'AI_GATEWAY_DEPLOY_MODE must be compose or kubernetes' >&2; exit 1 ;;
esac
remote_helper=${AI_GATEWAY_REMOTE_RELEASE_HELPER:-$default_remote_helper}
production_ssh_key=${AI_GATEWAY_PRODUCTION_SSH_KEY:-}
registry=${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc}
platform=linux/amd64
platform_probe_image=${AI_GATEWAY_PLATFORM_PROBE_IMAGE:-docker.m.daocloud.io/library/alpine:3.22}
status_file_override=${AI_GATEWAY_RELEASE_STATUS_FILE:-}

usage() {
  cat <<'EOF'
Usage:
  scripts/publish-release-images.sh [--components auto|api|web|all]

This command only builds, smoke-tests, and pushes immutable images. It never
changes production. When the user requested a production release, continue
with scripts/deploy-production-release.sh without requesting confirmation again.

Environment:
  AI_GATEWAY_PRODUCTION_HOST       SSH target used for read-only release status
  AI_GATEWAY_REMOTE_RELEASE_HELPER Fixed status/deploy helper on the server
  AI_GATEWAY_IMAGE_REGISTRY        Registry namespace
  AI_GATEWAY_RELEASE_STATUS_FILE   Local status manifest for offline/testing use
EOF
}

while [[ $# -gt 0 ]]; do
  case $1 in
    --components)
      [[ $# -ge 2 ]] || { echo 'missing value for --components' >&2; exit 64; }
      components=$2
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      printf 'unknown argument: %s\n' "$1" >&2
      usage >&2
      exit 64
      ;;
  esac
done

case $components in
  auto|api|web|all) ;;
  *) echo '--components must be auto, api, web, or all' >&2; exit 64 ;;
esac
[[ $production_host =~ ^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$ ]] || {
  echo 'AI_GATEWAY_PRODUCTION_HOST must use user@host syntax' >&2
  exit 1
}
[[ $remote_helper =~ ^/[A-Za-z0-9._/-]+$ ]] || {
  echo 'AI_GATEWAY_REMOTE_RELEASE_HELPER must be an absolute safe path' >&2
  exit 1
}
ssh_options=(-o BatchMode=yes -o ConnectTimeout=8)
if [[ -n $production_ssh_key ]]; then
  [[ $production_ssh_key == /* && -f $production_ssh_key && ! -L $production_ssh_key ]] || {
    echo 'AI_GATEWAY_PRODUCTION_SSH_KEY must be an absolute regular file' >&2
    exit 1
  }
  ssh_options+=(-i "$production_ssh_key")
fi
[[ $registry =~ ^[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._-]+)+$ ]] || {
  echo 'AI_GATEWAY_IMAGE_REGISTRY has an invalid format' >&2
  exit 1
}
[[ $platform_probe_image =~ ^[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._-]+)*(:[A-Za-z0-9._-]+|@sha256:[0-9a-f]{64})$ ]] || {
  echo 'AI_GATEWAY_PLATFORM_PROBE_IMAGE has an invalid format' >&2
  exit 1
}

fail() {
  echo "release_publish=FAIL $*" >&2
  exit 1
}

for command in git node docker curl; do
  command -v "$command" >/dev/null 2>&1 || fail "$command is required"
done
docker info >/dev/null 2>&1 || fail 'Docker Engine is not reachable'
docker buildx version >/dev/null 2>&1 || fail 'Docker Buildx is required'
docker compose version >/dev/null 2>&1 || fail 'Docker Compose v2 is required'

git fetch --quiet origin main
preflight_file=$(mktemp)
node scripts/release-preflight.mjs >"$preflight_file"
source_sha=$(node -e \
  'const value=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(value.sourceSha)' \
  "$preflight_file")

status_file=$(mktemp)
selection_file=$(mktemp)
smoke_project=easyai-release-smoke-${source_sha:0:12}
smoke_started=0

cleanup() {
  local status=$?
  trap - EXIT HUP INT TERM
  if [[ $smoke_started -eq 1 ]]; then
    COMPOSE_PROJECT_NAME=$smoke_project \
      docker compose -p "$smoke_project" -f docker-compose.yml down -v --remove-orphans \
      >/dev/null 2>&1 || true
  fi
  rm -f "$status_file" "$selection_file" "$preflight_file"
  exit "$status"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

has_status=0
if [[ -n $status_file_override ]]; then
  [[ -f $status_file_override ]] || fail 'AI_GATEWAY_RELEASE_STATUS_FILE does not exist'
  cp "$status_file_override" "$status_file"
  has_status=1
elif ssh "${ssh_options[@]}" "$production_host" \
  "$remote_helper" status >"$status_file" 2>/dev/null; then
  has_status=1
fi

base_sha=
current_api_image=
current_web_image=
if [[ $has_status -eq 1 ]]; then
  node scripts/release-manifest.mjs validate "$status_file" >/dev/null
  base_sha=$(node scripts/release-manifest.mjs get "$status_file" sourceSha)
  current_api_image=$(node scripts/release-manifest.mjs get "$status_file" images.api)
  current_web_image=$(node scripts/release-manifest.mjs get "$status_file" images.web)
fi

if [[ $components != all && $has_status -ne 1 ]]; then
  fail 'auto/api/web publishing requires a readable production release; use --components all for the first release'
fi

required_components=all
migrations_changed=true
base_verifiable=0
if [[ $has_status -eq 1 ]]; then
  if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
    git fetch --quiet origin "$base_sha" >/dev/null 2>&1 || true
  fi
  if git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
    git merge-base --is-ancestor "$base_sha" "$source_sha" || \
      fail 'the production release is not an ancestor of the source commit'
    base_verifiable=1
    node scripts/release-components.mjs "$base_sha" "$source_sha" >"$selection_file"
    required_components=$(node -e \
      'const value=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(value.components)' \
      "$selection_file")
    migrations_changed=$(node -e \
      'const value=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(String(value.migrationsChanged))' \
      "$selection_file")
  elif [[ $components != all ]]; then
    fail 'the production source SHA cannot be verified; rerun with --components all'
  else
    echo '[release] production source SHA is unavailable; conservatively building all components and running migrations' >&2
  fi
fi

if [[ $components == auto ]]; then
  components=$required_components
fi
if [[ $components == none ]]; then
  echo 'release_publish=SKIP reason=no_runtime_changes'
  exit 0
fi
if [[ $required_components == all && $components != all ]]; then
  fail "the source change requires both images; requested $components"
fi
if [[ $required_components == api && $components == web ]]; then
  fail 'the source change requires the API image'
fi
if [[ $required_components == web && $components == api ]]; then
  fail 'the source change requires the Web image'
fi
if [[ $migrations_changed == true && $components != api && $components != all ]]; then
  fail 'migration changes require the API image'
fi

api_repository=$registry/ai-gateway
web_repository=$registry/ai-gateway-web
api_tag=$api_repository:$source_sha
web_tag=$web_repository:$source_sha

remote_tag_must_not_exist() {
  local image=$1
  local output status
  set +e
  output=$(docker buildx imagetools inspect "$image" 2>&1)
  status=$?
  set -e
  if [[ $status -eq 0 ]]; then
    fail "immutable image tag already exists: $image"
  fi
  if ! grep -Eqi 'not found|manifest unknown|does not exist' <<<"$output"; then
    fail "cannot verify registry login and repository access for $image"
  fi
}

case $components in
  api) remote_tag_must_not_exist "$api_tag" ;;
  web) remote_tag_must_not_exist "$web_tag" ;;
  all)
    remote_tag_must_not_exist "$api_tag"
    remote_tag_must_not_exist "$web_tag"
    ;;
esac

docker run --rm --platform "$platform" "$platform_probe_image" true >/dev/null 2>&1 || \
  fail "Docker cannot execute $platform containers"

if [[ $base_verifiable -eq 1 ]]; then
  node scripts/ci-validate-migrations.mjs "$base_sha"
fi

echo '[release] running fast Go tests without database integration variables'
(
  cd apps/api
  env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1
)

api_image=$current_api_image
web_image=$current_web_image

build_image() {
  local target=$1
  local image=$2
  local -a build_args=()
  case $target in
    api)
      build_args=(
        --build-arg "GOPROXY=${AI_GATEWAY_GO_PROXY:-https://goproxy.cn,direct}"
        --build-arg "GO_BUILD_IMAGE=${AI_GATEWAY_GO_BUILD_IMAGE:-golang:1.26.3-alpine}"
        --build-arg "API_RUNTIME_IMAGE=${AI_GATEWAY_API_RUNTIME_IMAGE:-alpine:3.22}"
      )
      ;;
    web)
      build_args=(
        --build-arg "VITE_GATEWAY_API_BASE_URL=${AI_GATEWAY_WEB_API_BASE_URL:-/gateway-api}"
        --build-arg "NODE_BUILD_IMAGE=${AI_GATEWAY_NODE_BUILD_IMAGE:-node:22-alpine}"
        --build-arg "WEB_RUNTIME_IMAGE=${AI_GATEWAY_WEB_RUNTIME_IMAGE:-nginx:1.27-alpine}"
        --build-arg "NPM_CONFIG_REGISTRY=${AI_GATEWAY_NPM_REGISTRY:-https://registry.npmjs.org}"
        --build-arg "VITE_BASE_PATH=${AI_GATEWAY_WEB_BASE_PATH:-/}"
      )
      ;;
  esac
  echo "[release] building $target for $platform: $image"
  docker buildx build \
    --platform "$platform" \
    --file Dockerfile \
    --target "$target" \
    --label "org.opencontainers.image.revision=$source_sha" \
    --label 'org.opencontainers.image.source=https://git.51easyai.com/BCAI/easyai-ai-gateway' \
    --tag "$image" \
    --load \
    "${build_args[@]}" \
    .
  [[ $(docker image inspect "$image" --format '{{.Os}}/{{.Architecture}}') == "$platform" ]] || \
    fail "$image is not $platform"
  [[ $(docker image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}') == "$source_sha" ]] || \
    fail "$image is missing the source revision label"
}

case $components in
  api)
    build_image api "$api_tag"
    api_image=$api_tag
    ;;
  web)
    build_image web "$web_tag"
    web_image=$web_tag
    ;;
  all)
    build_image api "$api_tag"
    build_image web "$web_tag"
    api_image=$api_tag
    web_image=$web_tag
    ;;
esac

for image in "$api_image" "$web_image"; do
  docker image inspect "$image" >/dev/null 2>&1 || docker pull --platform "$platform" "$image"
done
build_completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')

export COMPOSE_PROJECT_NAME=$smoke_project
export AI_GATEWAY_PLATFORM=$platform
export AI_GATEWAY_API_IMAGE=$api_image
export AI_GATEWAY_WEB_IMAGE=$web_image
export AI_GATEWAY_API_PORT=0
export AI_GATEWAY_WEB_PORT=0
export AI_GATEWAY_DB_PORT=0
export AI_GATEWAY_COMPOSE_APP_ENV=test
export AI_GATEWAY_COMPOSE_IDENTITY_MODE=hybrid
export AI_GATEWAY_COMPOSE_DATABASE_NAME=easyai_ai_gateway
export AI_GATEWAY_COMPOSE_PG_USER=easyai
export AI_GATEWAY_COMPOSE_PG_PASSWORD=release-smoke-postgres
export AI_GATEWAY_COMPOSE_DATABASE_URL='postgresql://easyai:release-smoke-postgres@postgres:5432/easyai_ai_gateway?sslmode=disable'
export CONFIG_JWT_SECRET=release-smoke-$source_sha

smoke_started=1
docker compose -p "$smoke_project" -f docker-compose.yml up -d --pull never postgres
for _ in $(seq 1 60); do
  postgres_id=$(docker compose -p "$smoke_project" -f docker-compose.yml ps -q postgres)
  postgres_health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$postgres_id" 2>/dev/null || true)
  [[ $postgres_health == healthy ]] && break
  sleep 1
done
[[ ${postgres_health:-} == healthy ]] || fail 'ephemeral PostgreSQL did not become healthy'

docker compose -p "$smoke_project" -f docker-compose.yml run --rm --no-deps migrator
docker compose -p "$smoke_project" -f docker-compose.yml up -d --pull never --no-build api web
api_port=$(docker compose -p "$smoke_project" -f docker-compose.yml port api 8088 | tail -n 1)
web_port=$(docker compose -p "$smoke_project" -f docker-compose.yml port web 80 | tail -n 1)
api_port=${api_port##*:}
web_port=${web_port##*:}
[[ $api_port =~ ^[0-9]+$ && $web_port =~ ^[0-9]+$ ]] || fail 'could not resolve smoke ports'

RELEASE_SMOKE_BASE_URL=http://127.0.0.1:$api_port \
RELEASE_SMOKE_WEB_URL=http://127.0.0.1:$web_port \
RELEASE_SMOKE_COMPOSE_PROJECT=$smoke_project \
RELEASE_SMOKE_COMPOSE_FILE=$root/docker-compose.yml \
RELEASE_SMOKE_NONCE=${source_sha:0:12} \
RELEASE_SMOKE_DATABASE_USER=$AI_GATEWAY_COMPOSE_PG_USER \
RELEASE_SMOKE_DATABASE_NAME=$AI_GATEWAY_COMPOSE_DATABASE_NAME \
  node scripts/api-release-smoke.mjs
smoke_completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')

push_and_resolve() {
  local image=$1
  local repository=$2
  local resolved
  docker push "$image" >&2
  resolved=$(docker image inspect "$image" --format '{{range .RepoDigests}}{{println .}}{{end}}' | \
    awk -v prefix="$repository@sha256:" 'index($0, prefix) == 1 { print; exit }')
  [[ $resolved =~ ^[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._-]+)+@sha256:[0-9a-f]{64}$ ]] || \
    fail "could not resolve registry digest for $image"
  printf '%s\n' "$resolved"
}

case $components in
  api)
    api_image=$(push_and_resolve "$api_tag" "$api_repository")
    component_list=api
    ;;
  web)
    web_image=$(push_and_resolve "$web_tag" "$web_repository")
    component_list=web
    ;;
  all)
    api_image=$(push_and_resolve "$api_tag" "$api_repository")
    web_image=$(push_and_resolve "$web_tag" "$web_repository")
    component_list=api,web
    ;;
esac

mkdir -p dist/releases
manifest=$root/dist/releases/$source_sha.json
[[ ! -e $manifest ]] || fail "release manifest already exists: $manifest"
migration_file=$(git ls-files 'apps/api/migrations/*.sql' | LC_ALL=C sort | tail -n 1)
migration_version=${migration_file##*/}
migration_version=${migration_version%.sql}
[[ $migration_version =~ ^[0-9]{4}_[a-z0-9][a-z0-9_]*$ ]] ||
  fail 'could not resolve the latest database migration version'
RELEASE_SOURCE_SHA=$source_sha \
RELEASE_BASE_SHA=$base_sha \
RELEASE_COMPONENTS=$component_list \
RELEASE_MIGRATIONS_CHANGED=$migrations_changed \
RELEASE_MIGRATION_VERSION=$migration_version \
RELEASE_API_IMAGE=$api_image \
RELEASE_WEB_IMAGE=$web_image \
RELEASE_BUILD_COMPLETED_AT=$build_completed_at \
RELEASE_SMOKE_COMPLETED_AT=$smoke_completed_at \
  node scripts/release-manifest.mjs create "$manifest"

echo "release_publish=PASS release=$source_sha components=$component_list"
echo "release_manifest=$manifest"
echo "api_image=$api_image"
echo "web_image=$web_image"
echo 'production_changed=false'
