#!/usr/bin/env bash
# clean.sh -- remove everything this sample created: the demo sandbox, the echo API,
# the Helm release and its namespaces (including the credential namespace and its
# generated Secrets), and the admission adapter. It leaves the shared isolated-sandbox RuntimeClass and its approval label in
# place, and prints the command that removes the label.
#
# Usage:
#   ./scripts/clean.sh
set -uo pipefail

SAMPLE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
REPO_ROOT="$(cd "$SAMPLE_DIR/../../.." && pwd)"
SAMPLE="openshell-kubernetes"
TMP_DIR="$REPO_ROOT/tmp/$SAMPLE"
STATE_FILE="$TMP_DIR/state.env"

# .env first, so an operator's own names are the default; the run record then overrides
# with whatever setup.sh actually installed, which is what this script must act on. A
# clean up that read only the current .env, with no run record, has in the past acted
# against the wrong names on a shared cluster, which is exactly what this ordering
# prevents when the record exists.
if [[ -f "$SAMPLE_DIR/.env" ]]; then set -a; . "$SAMPLE_DIR/.env"; set +a; fi
if [[ -f "$STATE_FILE" ]]; then set -a; . "$STATE_FILE"; set +a; fi

GATEWAY_NAMESPACE="${GATEWAY_NAMESPACE:-openshell-kubernetes}"
# One release creates its sandboxes in its own namespace.
SANDBOX_NAMESPACE="$GATEWAY_NAMESPACE"
CREDENTIAL_NAMESPACE="${CREDENTIAL_NAMESPACE:-openshell-kubernetes-credentials}"
GATEWAY_RELEASE="${GATEWAY_RELEASE:-openshell}"
ADAPTER="${SAMPLE}-nitro-adapter"
incomplete=0

kc() { if [[ -n "${KUBE_CONTEXT:-}" ]]; then kubectl --context "$KUBE_CONTEXT" "$@"; else kubectl "$@"; fi; }
say() { printf '\n==> %s\n' "$*"; }

# sc_field <class> <jsonpath> : read one field of a StorageClass. Returns non-zero only
# when the read itself fails.
sc_field() {
  local class=$1 path=$2 out rc=0
  out="$(kc get storageclass "$class" --ignore-not-found -o "jsonpath=$path" 2>/dev/null)" || rc=$?
  printf '%s' "$out"
  return $rc
}

# Prints one of: absent (nothing to do, not a failure), owned (this sample's),
# foreign (exists, labelled as someone else's or not labelled at all), unknown (the
# lookup itself failed, which is never read as "gone").
ns_state() { # <namespace>
  local out rc=0
  out="$(kc get ns "$1" -o jsonpath='{.metadata.labels.app\.kubernetes\.io/managed-by}' 2>&1)" || rc=$?
  if [[ "$rc" == 0 ]]; then
    if [[ "$out" == "$SAMPLE" ]]; then echo owned; else echo foreign; fi
  elif echo "$out" | grep -q "NotFound"; then
    echo absent
  else
    echo unknown
  fi
}

# Waits for a namespace to actually disappear, so "deleting" is never the last word a
# failed or stuck delete gets. Prints nothing; sets $incomplete on a timeout.
wait_for_ns_gone() { # <namespace>
  local i state
  for i in $(seq 1 60); do
    state="$(ns_state "$1")"
    case "$state" in
      absent) return 0 ;;
      unknown)
        echo "  could not determine whether $1 is gone yet (the lookup itself failed); 'kubectl get ns $1' says why"
        incomplete=1
        return 1
        ;;
      # owned or foreign: it still exists and answered normally, so keep waiting.
    esac
    sleep 5
  done
  echo "  $1 did not finish terminating within 5 minutes; 'kubectl get ns $1 -o yaml' says what is still holding it"
  incomplete=1
}

sandbox_ns_state="$(ns_state "$SANDBOX_NAMESPACE")"
gateway_ns_state="$sandbox_ns_state"

say "the sandbox(es) this run created"
case "$sandbox_ns_state" in
  owned)
    sandboxes="$(kc get sandboxes.agents.x-k8s.io -n "$SANDBOX_NAMESPACE" -o name 2>/dev/null)" || {
      echo "  could not list sandboxes in $SANDBOX_NAMESPACE; left it incomplete"
      incomplete=1
    }
    for s in $sandboxes; do
      kc delete "$s" -n "$SANDBOX_NAMESPACE" --wait=false ||
        { echo "  could not delete $s"; incomplete=1; }
    done
    kc delete pod node-probe -n "$SANDBOX_NAMESPACE" --ignore-not-found --wait=false ||
      { echo "  could not delete pod/node-probe"; incomplete=1; }
    kc delete pod echo-api -n "$SANDBOX_NAMESPACE" --ignore-not-found --wait=false ||
      { echo "  could not delete pod/echo-api"; incomplete=1; }
    kc delete service echo-api -n "$SANDBOX_NAMESPACE" --ignore-not-found ||
      { echo "  could not delete service/echo-api"; incomplete=1; }
    ;;
  absent) echo "  $SANDBOX_NAMESPACE is already gone; nothing to do" ;;
  foreign) echo "  $SANDBOX_NAMESPACE exists and is not this sample's; left it alone"; incomplete=1 ;;
  unknown) echo "  could not determine whether $SANDBOX_NAMESPACE exists; left it alone"; incomplete=1 ;;
esac

say "the Helm release"
# A release already uninstalled by an earlier, interrupted clean up run (the namespace
# delete that follows it is what can fail and need a retry) is not a failure to report
# again: "helm status" says plainly whether there is a release left to remove.
# Only a successful uninstall, or helm's own "release: not found", counts as gone. Any other
# failure leaves release_gone=0, and the namespaces below are then left alone: deleting the
# namespace would remove Helm's release record with it and orphan whatever the release
# created outside that namespace.
release_gone=0
case "$gateway_ns_state" in
  owned)
    status_rc=0
    status_out="$(helm status "$GATEWAY_RELEASE" -n "$GATEWAY_NAMESPACE" ${KUBE_CONTEXT:+--kube-context "$KUBE_CONTEXT"} 2>&1)" || status_rc=$?
    if [[ "$status_rc" == 0 ]]; then
      helm uninstall "$GATEWAY_RELEASE" -n "$GATEWAY_NAMESPACE" ${KUBE_CONTEXT:+--kube-context "$KUBE_CONTEXT"} 2>&1 | sed 's/^/  /'
      if [[ "${PIPESTATUS[0]}" == 0 ]]; then release_gone=1; else echo "  helm uninstall $GATEWAY_RELEASE failed"; incomplete=1; fi
    elif grep -qx 'Error: release: not found' <<<"$status_out"; then
      # Helm's own message, matched as a whole line: a different error that merely
      # contains the phrase is "could not determine", not "gone".
      echo "  $GATEWAY_RELEASE already uninstalled"
      release_gone=1
    else
      echo "  could not determine whether $GATEWAY_RELEASE is installed ('helm status' said: $status_out); left it alone"
      incomplete=1
    fi
    ;;
  absent) echo "  $GATEWAY_NAMESPACE is already gone; nothing to do"; release_gone=1 ;;
  foreign) echo "  $GATEWAY_NAMESPACE exists and is not this sample's; left its release alone"; incomplete=1 ;;
  unknown) echo "  could not determine whether $GATEWAY_NAMESPACE exists; left its release alone"; incomplete=1 ;;
esac

say "the admission adapter"
for kind in mutatingadmissionpolicybinding mutatingadmissionpolicy; do
  # Existence first, then the label: an object that exists with no managed-by label at
  # all would otherwise print the same empty string an absent object prints, and be
  # mistaken for already gone.
  existing="$(kc get "$kind" "$ADAPTER" --ignore-not-found -o name 2>/dev/null)" ||
    { echo "  could not determine whether $kind $ADAPTER exists; left it alone"; incomplete=1; continue; }
  if [[ -z "$existing" ]]; then
    continue
  fi
  owner="$(kc get "$kind" "$ADAPTER" -o jsonpath='{.metadata.labels.app\.kubernetes\.io/managed-by}' 2>/dev/null)"
  if [[ "$owner" == "$SAMPLE" ]]; then
    kc delete "$kind" "$ADAPTER" || { echo "  could not delete $kind $ADAPTER"; incomplete=1; }
  else
    echo "  $kind $ADAPTER is not this sample's (managed-by='$owner'); left it alone"
    incomplete=1
  fi
done

say "the namespaces"
for ns in "$CREDENTIAL_NAMESPACE" "$GATEWAY_NAMESPACE"; do
  if [[ "$release_gone" != 1 ]]; then
    echo "  the Helm release is not confirmed gone; left $ns alone"
    incomplete=1
    continue
  fi
  case "$(ns_state "$ns")" in
    owned)
      echo "  deleting $ns"
      kc delete namespace "$ns" --wait=false || { echo "  could not delete namespace $ns"; incomplete=1; continue; }
      wait_for_ns_gone "$ns"
      ;;
    absent) echo "  $ns is already gone" ;;
    foreign) echo "  $ns exists and is not this sample's; left it alone"; incomplete=1 ;;
    unknown) echo "  could not determine whether $ns exists; left it alone"; incomplete=1 ;;
  esac
done

SAMPLE_STORAGE_CLASS="openshell-kubernetes-gp3"

say "the sample's own StorageClass, if the sample made it"
# Delete openshell-kubernetes-gp3 only when it exists AND carries the sample's mark. Never
# touch the cluster default (setup used it without creating anything), and never touch a
# class with this name that something else owns.
#
# Skipped while anything above is unconfirmed, for the same reason the namespace
# is: a claim of the sample's may still be bound to this class, and deleting a
# class a pending claim names would leave that claim stuck.
if [[ $incomplete -ne 0 ]]; then
  skip_sc=1
  echo "  SKIPPED: not deleting storageclass/$SAMPLE_STORAGE_CLASS while $incomplete item(s) above are unconfirmed; resolve them and re-run." >&2
else
  skip_sc=0
  sc_prov="$(sc_field "$SAMPLE_STORAGE_CLASS" '{.provisioner}')" ; sc_rc=$?
fi
if [[ $skip_sc -eq 1 ]]; then
  :
elif [[ $sc_rc -ne 0 ]]; then
  echo "  could not read storageclass/$SAMPLE_STORAGE_CLASS; not reporting it gone"
  incomplete=1
elif [[ -z "$sc_prov" ]]; then
  echo "  storageclass/$SAMPLE_STORAGE_CLASS not present (setup used the cluster default, or it was already removed)"
else
  sc_mark="$(sc_field "$SAMPLE_STORAGE_CLASS" '{.metadata.labels.app\.kubernetes\.io/managed-by}')" ; scm_rc=$?
  if [[ $scm_rc -ne 0 ]]; then
    echo "  could not read the labels on storageclass/$SAMPLE_STORAGE_CLASS; not deleting something I cannot verify"
    incomplete=1
  elif [[ "$sc_mark" != "$SAMPLE" ]]; then
    echo "  storageclass/$SAMPLE_STORAGE_CLASS exists without the sample's mark; leaving it (it is not the sample's)"
    incomplete=1
  else
    if kc delete storageclass "$SAMPLE_STORAGE_CLASS" --ignore-not-found --wait=true --timeout=60s; then
      echo "  storageclass/$SAMPLE_STORAGE_CLASS deleted"
    else
      echo "  storageclass/$SAMPLE_STORAGE_CLASS did not delete cleanly"
      incomplete=1
    fi
  fi
fi

say "left in place on purpose"
cat <<EOF
  The isolated-sandbox RuntimeClass and its openshell.ai/sandbox-attachable=true label:
  other OpenShell gateways and other samples on this cluster share it. To remove the
  label (only do this if nothing else on the cluster needs it):

    kubectl label runtimeclass isolated-sandbox openshell.ai/sandbox-attachable-

  This run's local scratch files under tmp/openshell-kubernetes/ (the downloaded CLI and
  charts, and the CLI's own client certificate for this gateway): gitignored, and not a
  cluster resource, so this script leaves them for the next run to reuse. Delete
  tmp/openshell-kubernetes/ yourself if you want them gone too.
EOF

if [[ "$incomplete" != 0 ]]; then
  echo
  echo "ERROR: clean up could not remove everything; see above." >&2
  exit 1
fi
rm -f "$STATE_FILE"
