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

usage() {
  cat <<'EOF' >&2
usage: agentteam-prod-activate-release.sh <bundle.tar.gz> <checksums.txt> <release-id>

Environment overrides:
  AGENTTEAM_PROD_ROOT                (default: /opt/agentteam)
  AGENTTEAM_PROD_ENV_FILE            (default: /etc/agentteam/agentteam.env)
  AGENTTEAM_PROD_ENVIRONMENT         (default: production)
  AGENTTEAM_PROD_SERVICE_NAME        (default: agentteam-server.service)
  AGENTTEAM_PROD_OWNER               (default: inferred from current release ownership, else agentteam)
  AGENTTEAM_PROD_GROUP               (default: $AGENTTEAM_PROD_OWNER)
  AGENTTEAM_PROD_HEALTHZ_URL         (default: derived from AGENTTEAM_HTTP_ADDRESS in env; fallback: http://127.0.0.1:3001/healthz)
  AGENTTEAM_PROD_READYZ_URL          (default: derived from AGENTTEAM_HTTP_ADDRESS in env; fallback: http://127.0.0.1:3001/readyz)
  AGENTTEAM_PROD_APP_URL             (default: unset; skip public URL check)
  AGENTTEAM_PROD_HEALTHCHECK_ATTEMPTS (default: 30)
  AGENTTEAM_PROD_HEALTHCHECK_INTERVAL_SEC (default: 2)

This script accepts either release-bundle web layout:
  bin/agentteam-server
  bin/agentteam-schema
  bin/agentteam-tidb-admin
  release.env   (optional; AGENTTEAM_DAEMON_MIN_VERSION must match the managed env)
  web/index.html
  web/dist/index.html

The managed environment is deployment-owned. This script only reads and
validates it; activation never edits or backs it up.
EOF
}

log() {
  printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S%z')" "$*"
}

fail() {
  printf '%s\n' "$*" >&2
  exit 1
}

on_error() {
  local exit_code="$?"
  local line="$1"
  local cmd="$2"
  fail "agentteam prod activate release failed at line ${line}: ${cmd} (exit=${exit_code})"
}

trap 'on_error ${LINENO} "${BASH_COMMAND}"' ERR

require_command() {
  local cmd="$1"
  command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd"
}

require_file() {
  local path="$1"
  [[ -f "$path" ]] || fail "required file not found: $path"
}

require_executable() {
  local path="$1"
  [[ -x "$path" ]] || fail "required executable not found: $path"
}

trim_left() {
  local value="$1"
  value="${value#"${value%%[![:space:]]*}"}"
  printf '%s' "$value"
}

trim_right() {
  local value="$1"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "$value"
}

read_env_value() {
  local env_file_path="$1"
  local key="$2"

  local line=''
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    line="$(trim_left "$line")"
    if [[ -z "$line" || "$line" == \#* ]]; then
      continue
    fi

    if [[ "$line" == export[[:space:]]* ]]; then
      line="${line#export}"
      line="$(trim_left "$line")"
    fi

    if [[ "$line" != "$key="* ]]; then
      continue
    fi

    local raw="${line#"$key="}"
    raw="$(trim_left "$raw")"
    if [[ -z "$raw" ]]; then
      return 0
    fi

    local first="${raw:0:1}"
    if [[ "$first" == "'" ]]; then
      raw="${raw:1}"
      if [[ "$raw" != *"'"* ]]; then
        echo "error: failed to parse $key (unterminated single quote) in env file: $env_file_path" >&2
        exit 1
      fi
      printf '%s' "${raw%%"'"*}"
      return 0
    fi
    if [[ "$first" == '"' ]]; then
      raw="${raw:1}"
      if [[ "$raw" != *'"'* ]]; then
        echo "error: failed to parse $key (unterminated double quote) in env file: $env_file_path" >&2
        exit 1
      fi
      printf '%s' "${raw%%\"*}"
      return 0
    fi

    raw="$(trim_right "$raw")"
    printf '%s' "$raw"
    return 0
  done <"$env_file_path"

  return 1
}

agentteam_base_url_from_env_file() {
  local env_file_path="$1"
  local address=''
  local port=''

  if ! address="$(read_env_value "$env_file_path" 'AGENTTEAM_HTTP_ADDRESS')"; then
    return 1
  fi
  address="$(trim_left "$address")"
  address="$(trim_right "$address")"
  [[ -n "$address" ]] || return 1

  if [[ "$address" == http://* || "$address" == https://* ]]; then
    printf '%s' "${address%/}"
    return 0
  fi
  if [[ "$address" == :* ]]; then
    port="${address#:}"
  else
    port="${address##*:}"
  fi
  [[ "$port" =~ ^[0-9]+$ ]] || return 1
  printf 'http://127.0.0.1:%s' "$port"
}

validate_release_env_metadata() {
  local release_dir="$1"
  local release_env_path="${release_dir}/release.env"
  local release_daemon_min_version=''
  local managed_daemon_min_version=''

  if [[ ! -f "$release_env_path" ]]; then
    return 0
  fi

  if release_daemon_min_version="$(read_env_value "$release_env_path" 'AGENTTEAM_DAEMON_MIN_VERSION')"; then
    managed_daemon_min_version="$(read_env_value "$ENV_FILE" 'AGENTTEAM_DAEMON_MIN_VERSION' || true)"
    [[ "$managed_daemon_min_version" == "$release_daemon_min_version" ]] || \
      fail "AGENTTEAM_DAEMON_MIN_VERSION in ${ENV_FILE} must match release metadata"
  fi
}

resolve_prod_environment() {
  printf '%s' "${AGENTTEAM_PROD_ENVIRONMENT:-production}"
}

validate_prod_environment() {
  local expected_environment=''
  local managed_environment=''
  expected_environment="$(resolve_prod_environment)"
  [[ "$expected_environment" != *$'\n'* ]] || fail "AGENTTEAM_PROD_ENVIRONMENT must not contain newlines"
  managed_environment="$(read_env_value "$ENV_FILE" 'AGENTTEAM_ENV' || true)"
  [[ "$managed_environment" == "$expected_environment" ]] || \
    fail "AGENTTEAM_ENV in ${ENV_FILE} must be ${expected_environment}"
}

export_env_value_from_file() {
  local env_file_path="$1"
  local key="$2"
  local value=''

  if value="$(read_env_value "$env_file_path" "$key")"; then
    export "${key}=${value}"
  else
    unset "$key"
  fi
}

load_schema_env() {
  local env_file_path="$1"

  # /etc/agentteam/agentteam.env is parsed by systemd as an EnvironmentFile, not by a shell.
  # Export the schema binary's actual dependencies without sourcing the file, so
  # URL query strings, secrets, and other systemd-valid values cannot be executed
  # or truncated by shell metacharacters such as '&'.
  export_env_value_from_file "$env_file_path" 'AGENTTEAM_ENV'
  export_env_value_from_file "$env_file_path" 'AGENTTEAM_DATABASE_DSN'
  export_env_value_from_file "$env_file_path" 'AGENTTEAM_CORE_STORAGE_DSN'
  export_env_value_from_file "$env_file_path" 'AGENTTEAM_DATABASE_CONNECT_TIMEOUT'
  export_env_value_from_file "$env_file_path" 'AGENTTEAM_MESHX_ENABLED'
  export_env_value_from_file "$env_file_path" 'MESHX_DATABASE_DSN'
  export_env_value_from_file "$env_file_path" 'MESHX_AGS_DATABASE_DSN'
  export_env_value_from_file "$env_file_path" 'MESHX_AGS_EMBEDDING_DIMENSIONS'
  export_env_value_from_file "$env_file_path" 'EMBEDDING_DIMENSIONS'
}

validate_service_env_file() {
  local service_name="$1"
  local expected_env_file="$2"
  local env_files=''
  local token=''
  local candidate=''
  local found=0

  if ! env_files="$(systemctl show "$service_name" -p EnvironmentFiles --value 2>/dev/null)"; then
    log "unable to inspect EnvironmentFiles for ${service_name}; continuing and relying on restart diagnostics"
    return 0
  fi

  for token in $env_files; do
    case "$token" in
      /*|-/*)
        candidate="${token#-}"
        if [[ "$candidate" == "$expected_env_file" ]]; then
          found=1
          break
        fi
        ;;
    esac
  done

  [[ "$found" == 1 ]] || fail "service ${service_name} does not load expected env file ${expected_env_file} (EnvironmentFiles=${env_files:-<none>})"
}

detect_owner_group() {
  local probe

  for probe in "$@"; do
    if stat -Lc '%U:%G' "$probe" >/dev/null 2>&1; then
      stat -Lc '%U:%G' "$probe"
      return 0
    fi
  done

  printf '%s\n' 'agentteam:agentteam'
}

extract_expected_sha() {
  local checksum_path="$1"
  local archive_name="$2"

  awk -v name="$archive_name" '
    {
      entry=$2
      sub(/^\*/, "", entry)
      gsub(/^.*\//, "", entry)
      if (entry == name) {
        print $1
        exit
      }
    }
  ' "$checksum_path"
}

wait_http_ready() {
  local label="$1"
  local url="$2"
  local attempts="$3"
  local interval_sec="$4"
  local last_error_file

  last_error_file="$(mktemp "${TMPDIR:-/tmp}/agentteam-prod-healthcheck.XXXXXX")"

  local i
  for i in $(seq 1 "$attempts"); do
    if curl --fail --silent --show-error --max-time 2 "$url" >/dev/null 2>"$last_error_file"; then
      rm -f "$last_error_file"
      return 0
    fi
    sleep "$interval_sec"
  done

  if [[ -s "$last_error_file" ]]; then
    cat "$last_error_file" >&2
  fi
  rm -f "$last_error_file"
  fail "${label} check failed after ${attempts} attempts: ${url}"
}

normalize_release_layout() {
  local release_dir="$1"
  local web_dir="$release_dir/web"
  local root_index="$web_dir/index.html"
  local legacy_dist="$web_dir/dist"
  local legacy_index="$legacy_dist/index.html"

  if [[ -f "$root_index" && ! -e "$legacy_dist" ]]; then
    ln -sfn . "$legacy_dist"
  fi

  [[ -f "$root_index" || -f "$legacy_index" ]] || \
    fail "release archive missing web/index.html or web/dist/index.html"
}

dump_service_diagnostics() {
  local service_name="$1"

  log "service ${service_name} is not active; dumping diagnostics"
  systemctl status --no-pager -l "$service_name" || true
  if command -v journalctl >/dev/null 2>&1; then
    journalctl -u "$service_name" -n 80 --no-pager -l || true
  fi
}

if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
  usage
  exit 0
fi

if [[ $# -ne 3 ]]; then
  usage
  exit 1
fi

readonly BUNDLE_PATH="$1"
readonly CHECKSUM_PATH="$2"
readonly RELEASE_ID="$3"
readonly ROOT="${AGENTTEAM_PROD_ROOT:-/opt/agentteam}"
readonly RELEASES_DIR="${ROOT}/releases"
readonly CURRENT_LINK="${ROOT}/current"
readonly ENV_FILE="${AGENTTEAM_PROD_ENV_FILE:-/etc/agentteam/agentteam.env}"
readonly SERVICE_NAME="${AGENTTEAM_PROD_SERVICE_NAME:-agentteam-server.service}"
readonly DEFAULT_OWNER_GROUP="$(detect_owner_group "$CURRENT_LINK" "$RELEASES_DIR" "$ROOT")"
readonly DEFAULT_OWNER="${DEFAULT_OWNER_GROUP%%:*}"
readonly DEFAULT_GROUP="${DEFAULT_OWNER_GROUP#*:}"
readonly OWNER="${AGENTTEAM_PROD_OWNER:-$DEFAULT_OWNER}"
readonly GROUP="${AGENTTEAM_PROD_GROUP:-${AGENTTEAM_PROD_OWNER:-$DEFAULT_GROUP}}"
agentteam_http_base_url="$(agentteam_base_url_from_env_file "$ENV_FILE" || true)"
readonly HEALTHZ_URL="${AGENTTEAM_PROD_HEALTHZ_URL:-${agentteam_http_base_url:-http://127.0.0.1:3001}/healthz}"
readonly READYZ_URL="${AGENTTEAM_PROD_READYZ_URL:-${agentteam_http_base_url:-http://127.0.0.1:3001}/readyz}"
readonly APP_URL="${AGENTTEAM_PROD_APP_URL:-}"
readonly HEALTHCHECK_ATTEMPTS="${AGENTTEAM_PROD_HEALTHCHECK_ATTEMPTS:-30}"
readonly HEALTHCHECK_INTERVAL_SEC="${AGENTTEAM_PROD_HEALTHCHECK_INTERVAL_SEC:-2}"
readonly TARGET_DIR="${RELEASES_DIR}/${RELEASE_ID}"

activation_dir="${RELEASES_DIR}/.${RELEASE_ID}.activating.$$"
cleanup_dir="$activation_dir"
cleanup() {
  if [[ -n "${cleanup_dir}" && -d "${cleanup_dir}" ]]; then
    rm -rf "${cleanup_dir}"
  fi
}
trap cleanup EXIT

for cmd in awk cp curl ln mkdir mktemp mv rm sha256sum stat systemctl tar chown; do
  require_command "$cmd"
done

require_file "$BUNDLE_PATH"
require_file "$CHECKSUM_PATH"
require_file "$ENV_FILE"
validate_service_env_file "$SERVICE_NAME" "$ENV_FILE"

archive_name="$(basename "$BUNDLE_PATH")"
expected_sha="$(extract_expected_sha "$CHECKSUM_PATH" "$archive_name")"
[[ -n "$expected_sha" ]] || fail "checksum entry not found for ${archive_name} in ${CHECKSUM_PATH}"
actual_sha="$(sha256sum "$BUNDLE_PATH" | awk '{print $1}')"
[[ "$expected_sha" == "$actual_sha" ]] || fail "checksum mismatch for ${archive_name}"

mkdir -p "$RELEASES_DIR"
rm -rf "$activation_dir" "$TARGET_DIR"
mkdir -p "$activation_dir"

log "extracting ${archive_name} into ${activation_dir}"
tar -xzf "$BUNDLE_PATH" -C "$activation_dir"

require_executable "$activation_dir/bin/agentteam-server"
require_executable "$activation_dir/bin/agentteam-schema"
require_executable "$activation_dir/bin/agentteam-tidb-admin"
normalize_release_layout "$activation_dir"
validate_release_env_metadata "$activation_dir"
validate_prod_environment

log "stopping service ${SERVICE_NAME} before schema cutover"
systemctl stop "$SERVICE_NAME"

log "running schema and workspace placement cutover gates with ${ENV_FILE}"
(
  load_schema_env "$ENV_FILE"

  [[ -n "${AGENTTEAM_DATABASE_DSN:-${AGENTTEAM_CORE_STORAGE_DSN:-}}" ]] || fail "AGENTTEAM_DATABASE_DSN is required in ${ENV_FILE}"

  "$activation_dir/bin/agentteam-schema" init
  "$activation_dir/bin/agentteam-tidb-admin" workspace-placement-backfill --workspace-group-id workspace-default
  "$activation_dir/bin/agentteam-tidb-admin" workspace-placement-verify --workspace-group-id workspace-default
)

mv "$activation_dir" "$TARGET_DIR"
cleanup_dir=""

ln -sfn "$TARGET_DIR" "$CURRENT_LINK"
chown -R "${OWNER}:${GROUP}" "$TARGET_DIR"
chown -h "${OWNER}:${GROUP}" "$CURRENT_LINK"

# Stop-the-world cutover for the channel attachment projection. The previous
# writer was stopped before schema initialization above, so it cannot create
# new attachments while the idempotent completeness gate reconciles any
# pre-cutover rows. The completeness assertion (missing=0, orphan=0) gates the
# new writer's start-up. On failure the service stays stopped (readers serve
# nothing rather than stale data); downtime for this cutover is authorized and
# is only the stop->schema/gates->start gap.
# Rollback: repoint CURRENT_LINK at the prior release and `systemctl start` it
# -- the old binary serves the legacy read path on the additive 00317 schema.
(
  load_schema_env "$ENV_FILE"
  [[ -n "${AGENTTEAM_DATABASE_DSN:-${AGENTTEAM_CORE_STORAGE_DSN:-}}" ]] || fail "AGENTTEAM_DATABASE_DSN is required in ${ENV_FILE}"
  "$CURRENT_LINK/bin/agentteam-tidb-admin" channel-attachment-projection-backfill
)

log "starting ${SERVICE_NAME}"
if ! systemctl start "$SERVICE_NAME"; then
  dump_service_diagnostics "$SERVICE_NAME"
  fail "service start failed: ${SERVICE_NAME}"
fi
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
  dump_service_diagnostics "$SERVICE_NAME"
  fail "service not active after start: ${SERVICE_NAME}"
fi

log "checking health endpoint ${HEALTHZ_URL}"
wait_http_ready "health" "$HEALTHZ_URL" "$HEALTHCHECK_ATTEMPTS" "$HEALTHCHECK_INTERVAL_SEC"

log "checking ready endpoint ${READYZ_URL}"
wait_http_ready "ready" "$READYZ_URL" "$HEALTHCHECK_ATTEMPTS" "$HEALTHCHECK_INTERVAL_SEC"

if [[ -n "$APP_URL" ]]; then
  log "checking app endpoint ${APP_URL}"
  wait_http_ready "app" "$APP_URL" "$HEALTHCHECK_ATTEMPTS" "$HEALTHCHECK_INTERVAL_SEC"
fi

# The channel attachment projection is verified complete before the new writer
# starts (stop-the-world gate above). After start the new writer maintains
# projection rows for every write via the reconcile hook, so no post-start
# catch-up is needed; the previous post-restart catch-up existed only for
# rolling-deploy gap writes that do not occur under stop-the-world.

rm -f "$BUNDLE_PATH" "$CHECKSUM_PATH"

echo "agentteam prod deploy success: ${RELEASE_ID}"
