#!/usr/bin/env bash
# vault-rescue.sh — end-to-end vault rescue for a single HQ user.
#
# Pulls the user's S3 vault to a tmp dir, runs the hq-sync rescue script
# against it at a target hq-core version, and pushes the result back to
# S3 with the hq-symlink:<target> wire protocol intact. The whole flow
# replicates what the menubar app would do if the user clicked "Update HQ"
# locally — except the operator runs it remotely against any vault they
# have AWS S3 access to.
#
# Usage:
#
#   vault-rescue.sh --prs <prs_*> --version <hq-version> [opts...]
#
# Required:
#   --prs <prs_*>          Entity UID (e.g. prs_01KQ7NTBRY8X2QAA4S8AAF26W6).
#                          Bucket name resolves to hq-vault-<lower-uid-w/-dashes>.
#   --version <X.Y.Z>      Target hq-core version. Becomes --ref v<X.Y.Z>.
#
# Optional:
#   --source <owner/repo>  Default: indigoai-us/hq-core (release repo, tagged).
#                          Override with indigoai-us/hq-core-staging for staging
#                          builds (no v-prefix; passes --ref main with the
#                          version used only for logging / future floor lookup).
#   --dry-run              Don't actually mutate S3. Pull still happens (so you
#                          can inspect the tmp dir), rescue runs --dry-run, push
#                          runs --dryrun. Combine with --keep-tmp to inspect
#                          what the rescue would have produced.
#   --keep-tmp             Don't delete the tmp dir on exit (useful for diffing).
#   --rescue-script <path> Override path to hq-sync's replace-rescue.sh.
#                          Default resolves via $HQ_RESCUE_SCRIPT, then
#                          <this-script>/../../hq-sync/scripts/replace-rescue.sh.
#   --profile <name>       AWS profile. Default: $HQ_VAULT_AWS_PROFILE.
#   --region <name>        AWS region. Default: us-east-1.
#   --tmp-dir <dir>        Override the tmp dir path. Default: mktemp under /tmp.
#   --floor-sha <40-hex>   Forwarded to replace-rescue.sh; bypasses the
#                          vault's stamped last_sync_sha.
#
# Auth: uses the caller's AWS profile directly. The menubar app vends
# short-lived STS creds via vault-service; this script does not — it
# requires standing S3 perms on the target bucket. Operator/admin only.
#
# Output: live logs from each phase, plus a final summary. The pre-update
# safety snapshot from replace-rescue.sh lands at
# ~/.hq/backups/pre-update-<ts>/ on the OPERATOR's machine, not the user's.

set -euo pipefail

PRS=""
VERSION=""
SOURCE_REPO="indigoai-us/hq-core"
DRY_RUN=0
KEEP_TMP=0
RESCUE_SCRIPT="${HQ_RESCUE_SCRIPT:-}"
AWS_PROFILE_OVERRIDE="${HQ_VAULT_AWS_PROFILE:-}"
AWS_REGION_OVERRIDE="${HQ_VAULT_AWS_REGION:-us-east-1}"
TMP_DIR_OVERRIDE=""
FLOOR_SHA=""

# Default rescue script: discover relative to this file's location. Works
# when hq-cloud and hq-sync are sibling apps under hq-workspace; falls back
# to the env var when checked out elsewhere.
if [ -z "$RESCUE_SCRIPT" ]; then
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  CANDIDATE="$SCRIPT_DIR/../../hq-sync/scripts/replace-rescue.sh"
  [ -f "$CANDIDATE" ] && RESCUE_SCRIPT="$CANDIDATE"
fi

usage() {
  sed -n '2,45p' "$0"
  exit 2
}

while [ $# -gt 0 ]; do
  case "$1" in
    --prs)            PRS="$2"; shift 2 ;;
    --version)        VERSION="$2"; shift 2 ;;
    --source)         SOURCE_REPO="$2"; shift 2 ;;
    --dry-run)        DRY_RUN=1; shift ;;
    --keep-tmp)       KEEP_TMP=1; shift ;;
    --rescue-script)  RESCUE_SCRIPT="$2"; shift 2 ;;
    --profile)        AWS_PROFILE_OVERRIDE="$2"; shift 2 ;;
    --region)         AWS_REGION_OVERRIDE="$2"; shift 2 ;;
    --tmp-dir)        TMP_DIR_OVERRIDE="$2"; shift 2 ;;
    --floor-sha)      FLOOR_SHA="$2"; shift 2 ;;
    -h|--help)        usage ;;
    *) echo "unknown arg: $1" >&2; usage ;;
  esac
done

[ -n "$PRS" ]     || { echo "error: --prs is required" >&2; usage; }
[ -n "$VERSION" ] || { echo "error: --version is required" >&2; usage; }
[ -n "$RESCUE_SCRIPT" ] || {
  echo "error: replace-rescue.sh not found. Pass --rescue-script <path> or set HQ_RESCUE_SCRIPT." >&2
  exit 2
}
[ -f "$RESCUE_SCRIPT" ] || {
  echo "error: --rescue-script '$RESCUE_SCRIPT' is not a file" >&2
  exit 2
}

case "$PRS" in
  prs_*) ;;
  *) echo "error: --prs must look like prs_*; got '$PRS'" >&2; exit 2 ;;
esac

# Bucket name convention (matches s3.ts entity → bucket mapping):
#   prs_01ABC...  →  hq-vault-prs-01abc...
BUCKET="hq-vault-$(printf '%s' "$PRS" | tr '[:upper:]' '[:lower:]' | tr '_' '-')"

# Construct rescue --ref. Tagged releases live in hq-core as v<version>;
# rolling staging uses main and treats --version as informational.
case "$SOURCE_REPO" in
  *hq-core-staging) REF="main" ;;
  *)                REF="v$VERSION" ;;
esac

# AWS CLI prefix.
AWS_ARGS=()
[ -n "$AWS_PROFILE_OVERRIDE" ] && AWS_ARGS+=(--profile "$AWS_PROFILE_OVERRIDE")
AWS_ARGS+=(--region "$AWS_REGION_OVERRIDE")

# Tmp dir setup.
if [ -n "$TMP_DIR_OVERRIDE" ]; then
  TMP_DIR="$TMP_DIR_OVERRIDE"
  mkdir -p "$TMP_DIR"
else
  TMP_DIR="$(mktemp -d -t hq-vault-rescue-XXXXXX)"
fi

# Trap MUST reference an unconditional global with default-empty fallback
# (set -u trips on undef refs in the trap body otherwise).
cleanup() {
  local rc=$?
  if [ "$KEEP_TMP" = "1" ]; then
    echo "==> --keep-tmp set; preserving $TMP_DIR" >&2
  elif [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then
    rm -rf "$TMP_DIR"
  fi
  return $rc
}
trap cleanup EXIT

cat <<EOF
==> vault-rescue
    prs:           $PRS
    bucket:        s3://$BUCKET
    target:        $SOURCE_REPO @ $REF  (version $VERSION)
    rescue script: $RESCUE_SCRIPT
    tmp dir:       $TMP_DIR
    dry-run:       $([ "$DRY_RUN" = "1" ] && echo "ON (no S3 mutation, no tmp mutation past pull)" || echo "off")
    keep-tmp:      $([ "$KEEP_TMP" = "1" ] && echo "yes" || echo "no")
EOF

# ---------- phase 1: pull ----------
echo "" >&2
echo "==> [1/3] pull s3://$BUCKET/  ->  $TMP_DIR/" >&2
aws "${AWS_ARGS[@]}" s3 sync "s3://$BUCKET/" "$TMP_DIR/" --quiet
echo "==> pull done. files: $(find "$TMP_DIR" -type f | wc -l | tr -d ' ')" >&2

# ---------- phase 2: rescue ----------
echo "" >&2
echo "==> [2/3] rescue: $RESCUE_SCRIPT --source $SOURCE_REPO --ref $REF --cloud-update" >&2

RESCUE_ARGS=(
  --hq-root "$TMP_DIR"
  --source "$SOURCE_REPO"
  --ref "$REF"
  --cloud-update
  --no-backup
  --yes
)
# --no-backup is intentional: replace-rescue.sh's own pre-update snapshot
# would land at ~/.hq/backups/ on the OPERATOR's machine, not the vault
# owner's — so it doesn't help the user we're rescuing. Phase 1 already
# leaves the pulled tmp tree on disk (preserved with --keep-tmp), which IS
# the meaningful pre-rescue snapshot. Operators who want a hardened
# backup tarball it themselves before invoking this script.
# Requires hq-sync ≥0.6.4 (indigoai-us/hq-sync#170 — earlier versions
# return exit 1 on the --no-backup branch and kill us mid-flight).
[ "$DRY_RUN" = "1" ] && RESCUE_ARGS+=(--dry-run)
[ -n "$FLOOR_SHA" ] && RESCUE_ARGS+=(--floor-sha "$FLOOR_SHA")

bash "$RESCUE_SCRIPT" "${RESCUE_ARGS[@]}"
echo "==> rescue done." >&2

if [ "$DRY_RUN" = "1" ]; then
  echo "" >&2
  echo "==> [3/3] push: SKIPPED (--dry-run; rescue ran --dry-run too, no tmp mutation to push)" >&2
  echo "==> vault-rescue complete (dry-run). Inspect $TMP_DIR." >&2
  KEEP_TMP=1
  exit 0
fi

# ---------- phase 3: push ----------
echo "" >&2
echo "==> [3/3] push $TMP_DIR/  ->  s3://$BUCKET/" >&2

# Build a staging tree: regular files copied verbatim; local symlinks
# serialized as hq-symlink:<target> marker files (no trailing newline,
# per apps/hq-cloud/src/s3.ts:417). Skip a minimal denylist that should
# never round-trip through S3.
STAGING="$(mktemp -d -t hq-vault-rescue-stage-XXXXXX)"
add_staging_cleanup() {
  rm -rf "${STAGING:-}"
}
# Chain cleanup: existing trap removes TMP_DIR; we also need STAGING gone.
trap '{ add_staging_cleanup; cleanup; }' EXIT

echo "    staging at $STAGING" >&2

# Denylist (path prefixes that should never push back). This is INTENTIONALLY
# smaller than apps/hq-cloud/src/ignore.ts — the vault we just pulled IS the
# source of truth, so over-filtering would cause spurious deletes.
deny_path() {
  case "$1" in
    .git|.git/*) return 0 ;;
    node_modules|node_modules/*) return 0 ;;
    target|target/*) return 0 ;;
    dist|dist/*) return 0 ;;
    build|build/*) return 0 ;;
    __pycache__|__pycache__/*) return 0 ;;
    .venv|.venv/*|venv|venv/*) return 0 ;;
    .DS_Store|*/.DS_Store) return 0 ;;
    .env|.env.*) return 0 ;;
    *.pyc|*.class) return 0 ;;
  esac
  return 1
}

n_files=0; n_symlinks=0; n_skipped=0
SYMLINK_LIST="$STAGING.symlinks.txt"
: > "$SYMLINK_LIST"

while IFS= read -r -d '' src; do
  rel="${src#"$TMP_DIR/"}"
  [ "$rel" = "$src" ] && continue
  [ -z "$rel" ] && continue
  if deny_path "$rel"; then
    n_skipped=$((n_skipped + 1))
    continue
  fi
  dest="$STAGING/$rel"
  mkdir -p "$(dirname "$dest")"
  if [ -L "$src" ]; then
    target="$(readlink "$src")"
    printf 'hq-symlink:%s' "$target" > "$dest"
    printf '%s\n' "$rel" >> "$SYMLINK_LIST"
    n_symlinks=$((n_symlinks + 1))
  elif [ -f "$src" ]; then
    cp -p "$src" "$dest"
    n_files=$((n_files + 1))
  fi
done < <(find "$TMP_DIR" \( -type d \( -name node_modules -o -name .git -o -name target -o -name dist -o -name build \) -prune \) -o \( \( -type f -o -type l \) -print0 \))

echo "    staged files=$n_files symlinks=$n_symlinks skipped=$n_skipped" >&2

# Sync to S3 — `--size-only` because staging mtime=now (fresh write) and
# S3 LastModified reflects original upload, so the default mtime-based
# comparator would treat every staging file as newer and re-upload it.
echo "    aws s3 sync $STAGING/ -> s3://$BUCKET/ --size-only --delete" >&2
aws "${AWS_ARGS[@]}" s3 sync "$STAGING/" "s3://$BUCKET/" --size-only --delete

# Stamp the hq-symlink metadata header on every symlink-marker object so
# the menubar pull materializes them as real symlinks (apps/hq-cloud/src/
# s3.ts:466-468 prefers header detection; body-sniff is fallback).
n_stamped=0
if [ -s "$SYMLINK_LIST" ]; then
  echo "    stamping hq-symlink-target metadata on $(wc -l < "$SYMLINK_LIST" | tr -d ' ') objects" >&2
  while IFS= read -r rel; do
    [ -z "$rel" ] && continue
    aws "${AWS_ARGS[@]}" s3api put-object \
      --bucket "$BUCKET" \
      --key "$rel" \
      --body "$STAGING/$rel" \
      --metadata "hq-symlink-target=1" \
      --content-type "text/plain" \
      >/dev/null
    n_stamped=$((n_stamped + 1))
  done < "$SYMLINK_LIST"
fi
echo "    stamped: $n_stamped" >&2

echo "" >&2
echo "==> vault-rescue complete." >&2
echo "    bucket:    s3://$BUCKET" >&2
echo "    version:   $VERSION ($SOURCE_REPO@$REF)" >&2
echo "    files:     $n_files" >&2
echo "    symlinks:  $n_symlinks" >&2
echo "    stamped:   $n_stamped" >&2
echo "    skipped:   $n_skipped" >&2
