#!/usr/bin/env bash
# unoverse destroy — take the whole deployment down, in one command.
#
# The counterpart to `deploy`, and the same shape: read the ground, show in plain English
# what will go, take ONE answer, then do exactly what was shown. It exists because the
# alternative is remembering `terraform destroy` in the right folder with the right
# environment, and because half a teardown is worse than none — a load balancer nobody
# knows about bills forever.
#
# THREE THINGS MAKE THIS SAFE.
#   1. It shows what exists before it asks anything.
#   2. It names what it will NOT remove: an adopted Postgres is somebody else's cluster,
#      read by this stack and never owned, so it survives. Saying so prevents both a
#      nasty surprise and a false sense that the bill is now zero.
#   3. Confirmation is the universe's NAME, typed. y/N is muscle memory; a name is a
#      decision, and this deletes data that no backup here can restore.
#
# It never touches the developer's terraform.tfvars, their .env, or their code. Only the
# cloud resources this ground owns.

# ── Resources a previous run created and did not write down ───────────────────────────────
#
# `destroy` removes what STATE records. A deploy killed mid-apply leaves AWS holding
# resources terraform never recorded, and they do not merely linger: a database or a load
# balancer keeps a security group alive, so the part terraform CAN do fails on
#
#   DependencyViolation: resource sg-... has a dependent object
#
# after fifteen minutes of retrying, and the teardown reports only that it did not finish.
# The operator is then left deleting a database, a Redis cluster and a load balancer by hand
# before a "one command" teardown will complete. That happened on 2026-08-05.
#
# IT IMPORTS. IT NEVER DELETES.
#
# Adopting an orphan into state is a state write and touches no cloud API. The deletion is
# still terraform's, from the plan the operator sees and confirms by name. That matters twice
# over: terraform already knows the order to take this stack apart (load balancer before its
# security group, database before the group holding its interface), and a second deletion
# engine written in bash would not, and would drift from main.tf the day a resource was
# added.
#
# NOTHING WE DID NOT CREATE, enforced four ways rather than trusted:
#
#   1. TAGGED AS OURS. `_cloud_orphans` lists only what carries `Universe=<name>`, and
#      `default_tags` stamps that on resources THIS configuration creates. A borrowed
#      database keeps its own tags and can never appear here (main.tf, "Borrowed resources
#      are never owned").
#   2. A KNOWN ADDRESS. The ARN must map to one of the handful of resources main.tf declares.
#      Anything else is left alone, whatever it is tagged.
#   3. NAMED AFTER THIS UNIVERSE. The ARN must contain the universe's own name, which is why
#      `aws_instance.app` is deliberately absent below: an EC2 ARN carries only an opaque
#      instance id, so it cannot be verified this way, and an unverifiable match is not one
#      worth making. An untracked server is reported instead.
#   4. NOT ALREADY TRACKED. An address present in state is skipped, so this can only ever add
#      what is missing, never re-point something the operator already owns.
#
# Failure is not fatal anywhere in here: the worst case is the teardown behaving exactly as
# it did before.
_adopt_orphans() {
  local dir="$1" uname_="$2" orphans arn addr id adopted=0 unclaimed=""

  command -v aws >/dev/null 2>&1 || return 0
  orphans=$(_cloud_orphans aws "$uname_" 2>/dev/null)
  [ -n "$orphans" ] && [ "$orphans" != "unknown" ] || return 0

  while IFS= read -r arn; do
    [ -n "$arn" ] || continue
    addr=""; id="$arn"
    case "$arn" in
      *:rds:*:db:"$uname_"-pg)                              addr="aws_db_instance.postgres";               id="$uname_-pg" ;;
      *:elasticache:*:replicationgroup:"$uname_"-redis)     addr="aws_elasticache_replication_group.redis"; id="$uname_-redis" ;;
      *:loadbalancer/app/"$uname_"-alb/*)                   addr="aws_lb.public" ;;
      *:targetgroup/"$uname_"-app/*)                        addr="aws_lb_target_group.app" ;;
      *:targetgroup/"$uname_"-canvas/*)                     addr="aws_lb_target_group.canvas[0]" ;;
      *)                                                    unclaimed="$unclaimed  $arn"$'\n' ; continue ;;
    esac
    terraform -chdir="$dir" state list 2>/dev/null | grep -qxF "$addr" && continue
    if terraform -chdir="$dir" import -input=false "$addr" "$id" >/dev/null 2>&1; then
      adopted=$((adopted + 1))
    else
      unclaimed="$unclaimed  $arn"$'\n'
    fi
  done <<EOF
$orphans
EOF

  [ "$adopted" -gt 0 ] && \
    info "Adopted $adopted resource(s) an interrupted run left untracked. They are in the plan below"
  # SAID OUT LOUD, never silently swept. Anything here is billing and this teardown will not
  # remove it, which the operator has to know BEFORE they are told the universe is gone.
  if [ -n "$unclaimed" ]; then
    warn "these carry this universe's tag but are not resources this teardown can remove:"
    printf '%s' "$unclaimed"
    echo "      Check them yourself before assuming the bill has stopped."
  fi
  return 0
}

cmd_destroy() {
  # NAME THE GROUND. This took the first configured one, and digitalocean is first in the
  # list — so an operator with both grounds who typed `unoverse destroy` meaning AWS would
  # have torn down DigitalOcean instead, with the confirmation prompt happily naming the
  # universe they thought they were destroying. _pick_ground refuses to guess when there is
  # more than one.
  local cloud
  cloud=$(SELF_CMD="unoverse destroy" _pick_ground "${1:-}") || {
    [ -f "$ROOT/infra/digitalocean/terraform.tfvars" ] || [ -f "$ROOT/infra/aws/terraform.tfvars" ] \
      || fail "No ground here. There is nothing deployed to take down"
    return 1
  }

  local dir="$ROOT/infra/$cloud"
  if [ ! -d "$dir/.terraform" ]; then
    fail "That ground was never applied. Nothing exists to destroy"
    return 1
  fi

  _ground_credentials
  _ensure_terraform || return 1

  local pretty
  [ "$cloud" = "aws" ] && pretty="AWS" || pretty="DigitalOcean"
  echo ""
  echo -e "  ${RED}${BOLD}⬡ Taking down your $pretty universe${NC}"
  echo ""

  local tmp planfile rc name
  tmp=$(mktemp -d); planfile="$tmp/plan"

  # The universe's name, read once. It is the tag every resource carries, so it is what
  # decides which untracked resources are this universe's (_adopt_orphans) as well as the
  # word the operator types to confirm.
  name=$(grep -E '^name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  name="${name:-universe}"

  # BEFORE THE PLAN, so what an interrupted run left behind is in the list the operator
  # confirms, rather than something they discover by hand an hour later.
  [ "$cloud" = "aws" ] && _adopt_orphans "$dir" "$name"

  # APPLY THE CONFIGURATION TO THE DATABASE FIRST, because destroy does not.
  #
  # `terraform destroy` deletes a resource as the last apply left it, so an attribute that
  # matters only AT DELETE — RDS's `skip_final_snapshot` — comes from STATE, not from
  # main.tf. Fixing main.tf therefore fixes the next universe and does nothing for one
  # already applied. Applying the config to the database writes those settings into state
  # and calls no API; the resource is deleted seconds later, so nothing else here matters.
  #
  # ONLY IF IT IS ALREADY IN STATE. Without that test this is not an update, it is a CREATE:
  # `apply -target` on a resource the configuration declares and the state does not have
  # builds it. So a teardown of a universe whose database had already gone — deleted by
  # hand, or removed from state — silently provisioned a NEW one, with -auto-approve, on the
  # way to being told to destroy everything. In state means the only possible change is to
  # the resource's own attributes.
  #
  # ANNOUNCED, because it runs before the first progress line and terraform can sit on an
  # RDS refresh for a while. Sending it to a log with nothing on screen made `unoverse
  # destroy` look frozen at the banner.
  if [ "$cloud" = "aws" ] \
    && terraform -chdir="$dir" state list 2>/dev/null | grep -qx "aws_db_instance.postgres"; then
    info "Checking the database's delete settings..."
    terraform -chdir="$dir" apply -input=false -auto-approve \
      -target=aws_db_instance.postgres >"$tmp/sync.log" 2>&1 \
      || warn "could not update them; the teardown may stop on the database"
  fi

  info "Working out what exists..."
  terraform -chdir="$dir" plan -destroy -input=false -detailed-exitcode -out="$planfile" >"$tmp/log" 2>&1
  rc=$?
  case "$rc" in
    0) ok "Nothing is deployed. There is nothing to take down"; rm -rf "$tmp"; return 0 ;;
    2) : ;;
    *) fail "Terraform could not read the deployment:"; tail -20 "$tmp/log" | sed 's/^/      /'; rm -rf "$tmp"; return 1 ;;
  esac

  if ! terraform -chdir="$dir" show -json "$planfile" 2>/dev/null | node "$GRAVITY_LIB/tfsummary.mjs"; then
    terraform -chdir="$dir" show "$planfile"
  fi

  # What SURVIVES matters as much as what goes: an adopted cluster is read, never owned.
  local kept
  kept=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  if [ -n "$kept" ]; then
    info "Staying: ${BOLD}$kept${NC} ${DIM}(you reused it, so this stack never owned it. Its bill continues)${NC}"
  fi
  if grep -qE '^byo_postgres_url[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null; then
    info "Staying: ${BOLD}your own database${NC} ${DIM}(byo_postgres_url — this stack never owned it)${NC}"
  fi

  echo ""
  # Say what is actually lost. With a reused cluster the DATABASE survives, and claiming
  # otherwise makes the warning untrue in the commonest case — which teaches people to
  # skim warnings.
  if [ -n "$kept" ]; then
    echo -e "  ${RED}This deletes the server and everything on its disk. It cannot be undone.${NC}"
    echo -e "  ${DIM}Your database and its contents survive in $kept.${NC}"
  else
    echo -e "  ${RED}This deletes the server, the database and all their data. It cannot be undone.${NC}"
  fi
  echo ""
  local typed
  read -r -p "  Type the universe name to confirm ($name): " typed
  if [ "$typed" != "$name" ]; then
    info "Name did not match. Nothing was destroyed"
    rm -rf "$tmp"
    return 1
  fi

  # Take our rule out of the borrowed cluster BEFORE the state goes, while the droplet id
  # is still readable. One rule, ours, never the list.
  if [ -n "$kept" ]; then
    local dropped
    dropped=$(terraform -chdir="$dir" state show digitalocean_droplet.app 2>/dev/null | awk -F'"' '/^ *id *=/{print $2; exit}')
    [ -n "$dropped" ] && _adopted_db_access "$cloud" revoke "$dropped"
  fi

  echo ""
  info "Taking it down..."
  echo ""
  terraform -chdir="$dir" apply -input=false "$planfile" || { fail "Teardown did not finish. Re-run: unoverse destroy"; rm -rf "$tmp"; return 1; }
  rm -rf "$tmp"

  # .env.production described a server that no longer exists. Leaving it makes the next
  # deploy think there is something to ship to.
  rm -f "$ROOT/.env.production"

  echo ""
  ok "Your $pretty universe is gone"
  info "Your ground, code and settings are untouched. ${BOLD}unoverse deploy${NC} builds it again"
  echo ""
}
