#!/usr/bin/env bash
# Executed as a short-lived Alibaba Cloud Assistant command. The GitHub
# workflow transmits this reviewed source as Base64 and passes only validated
# SHA, OSS object keys and content digests. It receives no long-lived secret.
set -euo pipefail

readonly RELEASE_SHA='{{git_sha}}'
readonly SERVER_KEY='{{server_key}}'
readonly SERVER_SHA='{{server_sha}}'
readonly WEB_KEY='{{web_key}}'
readonly WEB_SHA='{{web_sha}}'
readonly DAX_INVENTORY_KEY='{{dax_inventory_key}}'
readonly DAX_INVENTORY_SHA='{{dax_inventory_sha}}'
readonly WORKFLOW_RUN='{{workflow_run}}'
readonly ALLOW_MIGRATIONS='{{allow_migrations}}'
readonly COMPOSE_B64='{{compose_b64}}'
readonly STAGING_ROOT='/opt/helix/staging'
readonly COMPOSE_FILE="${STAGING_ROOT}/docker-compose.staging.yml"
readonly RUNTIME_ENV='/etc/helix/staging/runtime.env'
readonly DEPLOY_ENV='/etc/helix/staging/deploy.env'
readonly RECEIPT='/opt/helix/releases/staging-current.json'
readonly MIGRATION_ONLY_EXIT_CODE=42
readonly MIGRATION_REQUIRED_EXIT_CODE=43

log() { printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
fail() { log "ERROR: $*" >&2; exit 1; }
normalize_unexpected_error() {
  local status=$?
  log "ERROR: unexpected command failure (exit ${status})" >&2
  exit 1
}
trap normalize_unexpected_error ERR
require_regular_file() { [[ -f "$1" ]] || fail "required file is absent: $1"; }
require_mode_600() {
  local mode
  mode="$(stat -c '%a' "$1")"
  [[ "$mode" == '600' ]] || fail "$1 must be mode 0600 (got ${mode})"
}

require_sanitization_receipt() {
  local path="$1"
  require_regular_file "$path"
  case "$path" in
    /opt/helix/releases/staging-sanitization-*.json)
      jq -e '
        .schema_version == 1 and .environment == "staging" and
        (.sanitized_at | type == "string" and length > 0) and
        (.sanitizer_source_sha | type == "string" and test("^[0-9a-f]{40}$")) and
        (.database | type == "object" and .sanitized_from_snapshot == true) and
        (.effects | type == "object" and
          .scheduled_jobs_disabled == true and .integrations_disabled == true and
          .identity_targets_disabled == true and .provider_credentials_removed == true and
          .tokens_revoked == true and .provider_locators_removed == true and
          .people_pseudonymized == true and .synthetic_cohort_seeded == true)
      ' "$path" >/dev/null || \
        fail 'Staging sanitization receipt does not prove every isolation control'
      ;;
    /opt/helix/releases/staging-dax-retained-*.json)
      jq -e '
        .schema_version == 2 and .environment == "staging" and
        .receipt_type == "dax_retained_validation" and
        .retention_policy == "preserve_across_deploys" and
        .cleanup_required_before_next_deploy == false and
        (.sanitized_at | type == "string" and length > 0) and
        (.retained_at | type == "string" and length > 0) and
        (.live_readback_at | type == "string" and length > 0) and
        (.sanitizer_source_sha | type == "string" and test("^[0-9a-f]{40}$")) and
        (.source_git_sha | type == "string" and test("^[0-9a-f]{40}$")) and
        (.database | type == "object" and .sanitized_from_snapshot == true and
          (.alembic_revision | type == "string" and test("^[0-9]+$"))) and
        (.effects | type == "object" and
          .scheduled_jobs_disabled == true and .integrations_disabled == false and
          .identity_targets_disabled == true and .provider_credentials_removed == false and
          .tokens_revoked == true and .provider_locators_removed == false and
          .people_pseudonymized == true and .synthetic_cohort_seeded == true) and
        (.dax_safety | type == "object" and
          .retained_for_user_validation == true and .worker_enabled == false and
          .metadata_only_credential == true and .enabled_integration_count == 1 and
          .catalog_dataset_count >= 1 and
          .apply_mode_enabled == false and .personal_apply_enabled == false and
          .service_account_apply_enabled == false)
      ' "$path" >/dev/null || \
        fail 'Staging DAX retained receipt does not prove the metadata-only safety boundary'
      ;;
    *)
      fail 'invalid sanitization receipt path'
      ;;
  esac
}

healthy() {
  local url="$1" label="$2" attempt
  for attempt in $(seq 1 12); do
    if curl --fail --silent --show-error --max-time 5 "$url" >/dev/null; then
      log "healthy: ${label}"
      return 0
    fi
    sleep 5
  done
  fail "health check did not become ready: ${label}"
}

require_artifact() {
  local key="$1" digest="$2"
  [[ "$digest" =~ ^[0-9a-f]{64}$ ]] || fail 'artifact digest is invalid'
  [[ "$key" == "helix-staging/blobs/sha256/${digest}.tar.zst" ]] || fail 'artifact key is not content-addressed'
}

require_dax_inventory_artifact() {
  local key="$1" digest="$2"
  [[ "$digest" =~ ^[0-9a-f]{64}$ ]] || fail 'DAX inventory digest is invalid'
  [[ "$key" == "helix-staging/catalogs/sha256/${digest}.json" ]] || \
    fail 'DAX inventory key is not content-addressed'
}

require_regular_file "$RUNTIME_ENV"
require_regular_file "$DEPLOY_ENV"
require_mode_600 "$RUNTIME_ENV"

set -a
# shellcheck source=/dev/null
source "$DEPLOY_ENV"
set +a
: "${STAGING_OSS_BUCKET:?missing STAGING_OSS_BUCKET}"
: "${STAGING_OSS_ENDPOINT:?missing STAGING_OSS_ENDPOINT}"
: "${STAGING_OSS_REGION:?missing STAGING_OSS_REGION}"
: "${STAGING_ECS_RAM_ROLE:?missing STAGING_ECS_RAM_ROLE}"
: "${STAGING_DOCKER_NETWORK:?missing STAGING_DOCKER_NETWORK}"
: "${STAGING_REDIS_VOLUME:?missing STAGING_REDIS_VOLUME}"
: "${STAGING_SANITIZATION_RECEIPT:?missing STAGING_SANITIZATION_RECEIPT}"

[[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] || fail 'invalid release SHA'
[[ "$WORKFLOW_RUN" =~ ^[0-9]+-[0-9]+$ ]] || fail 'invalid workflow run identity'
[[ "$ALLOW_MIGRATIONS" == 'true' || "$ALLOW_MIGRATIONS" == 'false' ]] || fail 'invalid migration flag'
[[ "$COMPOSE_B64" =~ ^[A-Za-z0-9+/=]+$ ]] || fail 'invalid compose payload'
[[ "$STAGING_OSS_BUCKET" =~ ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$ ]] || fail 'invalid OSS bucket'
[[ "$STAGING_OSS_ENDPOINT" == "oss-${STAGING_OSS_REGION}.aliyuncs.com" ]] || fail 'invalid OSS endpoint'
[[ "$STAGING_ECS_RAM_ROLE" == 'HelixStagingRuntime' ]] || fail 'unexpected ECS RAM role'
[[ "$STAGING_DOCKER_NETWORK" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$ ]] || fail 'invalid Docker network'
[[ "$STAGING_REDIS_VOLUME" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$ ]] || fail 'invalid Redis volume'
require_artifact "$SERVER_KEY" "$SERVER_SHA"
require_artifact "$WEB_KEY" "$WEB_SHA"
require_dax_inventory_artifact "$DAX_INVENTORY_KEY" "$DAX_INVENTORY_SHA"
require_sanitization_receipt "$STAGING_SANITIZATION_RECEIPT"
docker network inspect "$STAGING_DOCKER_NETWORK" >/dev/null || fail 'required Docker network is absent'
docker volume inspect "$STAGING_REDIS_VOLUME" >/dev/null || fail 'required Redis volume is absent'

install -d -m 0755 "$STAGING_ROOT" "$(dirname "$RECEIPT")" /run/lock
exec 9>/run/lock/helix-staging-deploy.lock
flock -n 9 || fail 'another Staging deployment is already running on this host'

compose_tmp="${COMPOSE_FILE}.tmp.${WORKFLOW_RUN}"
printf '%s' "$COMPOSE_B64" | base64 --decode > "$compose_tmp" || fail 'could not decode compose payload'
chmod 0644 "$compose_tmp"
mv "$compose_tmp" "$COMPOSE_FILE"

artifact_dir="$(mktemp -d /opt/helix/staging/artifacts.XXXXXX)"
cleanup() { rm -rf -- "$artifact_dir"; }
trap cleanup EXIT

download_artifact() {
  local key="$1" expected="$2" output="$3"
  aliyun oss cp "oss://${STAGING_OSS_BUCKET}/${key}" "$output" --force \
    --mode EcsRamRole --ecs-role-name "$STAGING_ECS_RAM_ROLE" \
    --region "$STAGING_OSS_REGION" --endpoint "$STAGING_OSS_ENDPOINT" >/dev/null
  echo "${expected}  ${output}" | sha256sum --check --strict
}

log 'downloading exact content-addressed Staging image archives through the ECS RAM role'
download_artifact "$SERVER_KEY" "$SERVER_SHA" "$artifact_dir/server.tar.zst"
download_artifact "$WEB_KEY" "$WEB_SHA" "$artifact_dir/web.tar.zst"
download_artifact \
  "$DAX_INVENTORY_KEY" "$DAX_INVENTORY_SHA" "$artifact_dir/dax-inventory.json"
jq -e 'type == "object" and (.datasets | type == "array" and length > 0)' \
  "$artifact_dir/dax-inventory.json" >/dev/null || fail 'DAX inventory is invalid'
zstd -dc "$artifact_dir/server.tar.zst" | docker load >/dev/null
zstd -dc "$artifact_dir/web.tar.zst" | docker load >/dev/null

readonly SERVER_IMAGE="helix-staging-server:sha-${RELEASE_SHA}"
readonly WEB_IMAGE="helix-staging-web:sha-${RELEASE_SHA}"
server_revision="$(docker image inspect "$SERVER_IMAGE" | jq -er '.[0].Config.Labels["org.opencontainers.image.revision"]')"
web_revision="$(docker image inspect "$WEB_IMAGE" | jq -er '.[0].Config.Labels["org.opencontainers.image.revision"]')"
[[ "$server_revision" == "$RELEASE_SHA" ]] || fail 'server image revision does not match release SHA'
[[ "$web_revision" == "$RELEASE_SHA" ]] || fail 'web image revision does not match release SHA'

export STAGING_SERVER_IMAGE="$SERVER_IMAGE"
export STAGING_WEB_IMAGE="$WEB_IMAGE"
export STAGING_RUNTIME_ENV="$RUNTIME_ENV"
services="$(docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" -f "$COMPOSE_FILE" config --services | LC_ALL=C sort | tr '\n' ' ')"
[[ "$services" == 'backend frontend redis ' ]] || fail "Staging compose service allowlist changed: ${services}"

log 'ensuring isolated Staging Redis is healthy'
docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" -f "$COMPOSE_FILE" up -d redis
for attempt in $(seq 1 12); do
  if docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" -f "$COMPOSE_FILE" exec -T redis redis-cli ping | grep -qx PONG; then
    break
  fi
  sleep 5
  [[ "$attempt" -lt 12 ]] || fail 'Staging Redis did not become healthy'
done

schema_current="$(docker run --rm --env-file "$RUNTIME_ENV" --env PYTHONPATH=/app --network host "$SERVER_IMAGE" sh -c 'cd /app && /app/.venv/bin/alembic current' | awk '/^[0-9]+/ {print $1}')"
schema_head="$(docker run --rm --env-file "$RUNTIME_ENV" --env PYTHONPATH=/app --network host "$SERVER_IMAGE" sh -c 'cd /app && /app/.venv/bin/alembic heads' | awk '/^[0-9]+/ {print $1}')"
[[ "$schema_current" =~ ^[0-9]+$ ]] || fail "invalid current Alembic revision: ${schema_current:-empty}"
[[ "$schema_head" =~ ^[0-9]+$ ]] || fail "invalid release-head Alembic revision: ${schema_head:-empty}"
sanitization_revision="$(jq -er '.database.alembic_revision' "$STAGING_SANITIZATION_RECEIPT")"
[[ "$sanitization_revision" =~ ^[0-9]+$ ]] || fail 'sanitization receipt has an invalid Alembic revision'
[[ "$sanitization_revision" == "$schema_current" ]] || \
  fail "sanitization receipt covers schema ${sanitization_revision}, but Staging is ${schema_current}; re-sanitize or re-attest the current schema"
if [[ "$schema_current" != "$schema_head" ]]; then
  if [[ "$ALLOW_MIGRATIONS" != 'true' ]]; then
    # A standing precondition awaiting an authorized migration, not a runtime
    # regression. Exit code 43 is reserved for exactly this state so the
    # workflow can report it without parsing human-readable output; the
    # machine-readable line below carries both revisions for the summary.
    log "SCHEMA_BEHIND current=${schema_current} head=${schema_head}"
    log "ERROR: Staging schema ${schema_current} differs from release head ${schema_head}; this run is not authorized to migrate (dispatch with apply_migrations=true)"
    exit "$MIGRATION_REQUIRED_EXIT_CODE"
  fi
  log "applying reviewed Staging migration ${schema_current} -> ${schema_head}"
  docker run --rm --env-file "$RUNTIME_ENV" --env PYTHONPATH=/app --network host "$SERVER_IMAGE" sh -c 'cd /app && /app/.venv/bin/alembic upgrade head'
  migrated_revision="$(docker run --rm --env-file "$RUNTIME_ENV" --env PYTHONPATH=/app --network host "$SERVER_IMAGE" sh -c 'cd /app && /app/.venv/bin/alembic current' | awk '/^[0-9]+/ {print $1}')"
  [[ "$migrated_revision" == "$schema_head" ]] || fail "Staging migration stopped at ${migrated_revision:-unknown}, expected ${schema_head}"
  migration_receipt='/opt/helix/releases/staging-migration-pending.json'
  migration_receipt_tmp="${migration_receipt}.tmp.${WORKFLOW_RUN}"
  jq -n --arg from "$schema_current" --arg to "$schema_head" \
    --arg git_sha "$RELEASE_SHA" --arg workflow_run "$WORKFLOW_RUN" \
    --arg migrated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    '{environment:"staging",status:"migration_complete_revalidation_required",from_revision:$from,to_revision:$to,git_sha:$git_sha,workflow_run:$workflow_run,migrated_at:$migrated_at,runtime_replaced:false}' \
    > "$migration_receipt_tmp"
  chmod 0644 "$migration_receipt_tmp"
  mv "$migration_receipt_tmp" "$migration_receipt"
  log "migration-only phase complete; re-sanitize or re-attest schema ${schema_head}, update STAGING_SANITIZATION_RECEIPT, then dispatch with migrations off"
  exit "$MIGRATION_ONLY_EXIT_CODE"
fi

previous_server=''
previous_web=''
if [[ -f "$RECEIPT" ]]; then
  previous_server="$(jq -r '.server_image // empty' "$RECEIPT" 2>/dev/null || true)"
  previous_web="$(jq -r '.web_image // empty' "$RECEIPT" 2>/dev/null || true)"
fi
rollback() {
  local status=$?
  if [[ -n "$previous_server" && -n "$previous_web" ]]; then
    log 'deployment failed; attempting runtime-only rollback to previous local image tags'
    STAGING_SERVER_IMAGE="$previous_server" STAGING_WEB_IMAGE="$previous_web" \
      docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" -f "$COMPOSE_FILE" up -d --no-deps --force-recreate backend frontend || true
  fi
  exit "$status"
}
trap rollback ERR

log "recreating backend and frontend for ${RELEASE_SHA}"
docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" -f "$COMPOSE_FILE" up -d --no-deps --force-recreate backend frontend
healthy 'http://127.0.0.1:8000/api/v1/ready' 'backend readiness'
healthy 'http://127.0.0.1:8080/' 'frontend'

log 'projecting the release-bound DAX catalog with automatic lineage roots'
docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" \
  -f "$COMPOSE_FILE" cp \
  "$artifact_dir/dax-inventory.json" backend:/tmp/dax-dataset-inventory.json
seed_args=(
  /app/.venv/bin/python
  scripts/seed_data_access_resources.py
  /tmp/dax-dataset-inventory.json
  --source-version "staging-release-${RELEASE_SHA}"
  --commit
)
lineage_evidence_prefix=''
if docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" \
    -f "$COMPOSE_FILE" exec -T --workdir /app -e PYTHONPATH=/app backend \
    /app/.venv/bin/python scripts/seed_data_access_resources.py --help \
    | grep -q -- '--lineage-catalog'; then
  seed_args+=(
    --lineage-catalog
    /app/app/domains/data_access/lineage_catalogs/gcp-digital-bi-dbt.json
    --unresolved-owner-fallback-email
    xiaopeng.zhu@pingcap.com
  )
else
  referenced_roots="$(jq -er '.metadata.lineage_root_dataset_count | select(. > 0)' \
    "$artifact_dir/dax-inventory.json")"
  lineage_evidence_prefix="AUTOMATIC LINEAGE ROOTS: referenced=${referenced_roots} added=0"
fi
dax_seed_output="$(
  docker compose --project-name helix-staging --env-file "$RUNTIME_ENV" \
    -f "$COMPOSE_FILE" exec -T --workdir /app -e PYTHONPATH=/app backend \
    "${seed_args[@]}"
)"
if [[ -n "$lineage_evidence_prefix" ]]; then
  dax_seed_output="${lineage_evidence_prefix}"$'\n'"${dax_seed_output}"
fi
printf '%s\n' "$dax_seed_output"
grep -Eq '^AUTOMATIC LINEAGE ROOTS: referenced=[1-9][0-9]* added=[0-9]+$' \
  <<<"$dax_seed_output" || fail 'DAX lineage-root projection evidence is absent'
grep -Eq '^OK: entries=[1-9][0-9]* created=[0-9]+ updated=[0-9]+ unchanged=[0-9]+ suspended=[0-9]+$' \
  <<<"$dax_seed_output" || fail 'DAX catalog projection did not commit'
trap - ERR

receipt_tmp="${RECEIPT}.tmp.${WORKFLOW_RUN}"
jq -n \
  --arg git_sha "$RELEASE_SHA" --arg server_image "$SERVER_IMAGE" --arg web_image "$WEB_IMAGE" \
  --arg server_key "$SERVER_KEY" --arg server_sha "$SERVER_SHA" \
  --arg web_key "$WEB_KEY" --arg web_sha "$WEB_SHA" \
  --arg dax_inventory_key "$DAX_INVENTORY_KEY" \
  --arg dax_inventory_sha "$DAX_INVENTORY_SHA" \
  --arg workflow_run "$WORKFLOW_RUN" --arg schema "$schema_head" \
  --arg deployed_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  '{environment:"staging",git_sha:$git_sha,server_image:$server_image,web_image:$web_image,server_artifact:{key:$server_key,sha256:$server_sha},web_artifact:{key:$web_key,sha256:$web_sha},dax_inventory_artifact:{key:$dax_inventory_key,sha256:$dax_inventory_sha},dax_catalog_auto_projected:true,schema_revision:$schema,workflow_run:$workflow_run,deployed_at:$deployed_at,worker_enabled:false}' \
  > "$receipt_tmp"
chmod 0644 "$receipt_tmp"
mv "$receipt_tmp" "$RECEIPT"
log "Staging deployment complete: ${RELEASE_SHA}"
