# vast-gpu-life: lifecycle verbs for the vast-gpu front door.
# Sourced, never executed. Expects EXEC, CRED, ROOT, COOLDOWN, RATE_COOLDOWN,
# GUARD, VIEW and the front door's helpers to be in scope.
#
# These verbs exist because the lifecycle, not the provider API, is where an
# agent loses time: an unreported instance id, a key that has to be guessed, a
# status polled by hand in a shell loop, and a stopped box nobody destroyed.

life_doctor() {
  echo "executor: $EXEC"
  "$EXEC" --version 2>&1 | head -1
  if [ -r "$CRED" ] && grep -q '^VAST_API_KEY=..*' "$CRED" 2>/dev/null; then
    echo "credential file: $CRED  (VAST_API_KEY present)"
  else
    echo "credential file: $CRED  MISSING or has no VAST_API_KEY - nothing below will work"
  fi
  echo "evidence root: $ROOT ($(ls -1 "$ROOT" 2>/dev/null | wc -l | tr -d ' ') entries)"
  echo "ssh-keygen: $(command -v ssh-keygen || echo MISSING)"
  for k in ${VAST_SSH_PUBKEY:+"$VAST_SSH_PUBKEY"} "$HOME/.ssh/id_ed25519.pub"; do
    if [ -f "$k" ]; then echo "public key: $k  $(ssh-keygen -lf "$k" 2>/dev/null)"; else echo "public key absent: $k"; fi
  done
  echo "keys are normalized to 'ssh-ed25519 <blob>' before attach: a comment or trailing newline is dropped, not refused."
}

# One instance read whose coordinates are parsed once. Prints one tab-separated
# record: evidence dir, status, host, port.
life_coords() { # life_coords <instance-id>
  local iid="$1"
  local dir; dir="$(evidence_dir "instance-$iid")"
  local code
  set +e; capture_into_dir "$dir" vast instance --id "$iid"; code=$?; set -e
  local coords
  coords="$(view where "$dir" 2>/dev/null || true)"
  state record "$dir" instance "$code" "$(explain "$code")" \
    "$(printf '%s' "$coords" | grep -m1 '^status:' || true)" 0 >/dev/null
  if [ $code -ne 0 ]; then
    cat "$dir/stderr.txt" >&2
    echo "vast-gpu: instance read failed, exit $code: $(explain "$code")" >&2
    return 2
  fi
  [ -n "$coords" ] || return 2
  I_STATUS="$(sed -n 's/^status: //p' <<<"$coords")"
  I_HOST="$(sed -n 's/^host: //p' <<<"$coords")"
  I_PORT="$(sed -n 's/^port: //p' <<<"$coords")"
  # Command substitution runs in a subshell, so state cannot be returned in
  # globals: return the four fields as one tab-separated record instead.
  printf '%s\t%s\t%s\t%s' "$dir" "$I_STATUS" "$I_HOST" "$I_PORT"
}

# The ssh line always carries the bound identity. Skipping it is how an agent
# ends up testing candidate keys against a host, which reads as credential
# exploration and gets blocked.
life_ssh_line() { # life_ssh_line <instance-id>
  local iid="$1" idkey
  if idkey="$(key_for "$iid")"; then
    echo "identity: $idkey"
    echo "ssh -i $idkey -p $I_PORT -o StrictHostKeyChecking=accept-new root@$I_HOST"
  else
    echo "identity: UNKNOWN for $iid (no up/attach recorded on this machine)"
    echo "vast-gpu: do not probe candidate keys against the host. Attach one, then run 'vast-gpu ssh-verify $iid --key <pubkey>'."
    echo "ssh -p $I_PORT root@$I_HOST   # needs -i <the attached identity file>"
  fi
}

life_up() {
  local ask="" image="" onstart="" disk="" key="" label=""
  while [ $# -gt 0 ]; do
    case "$1" in
      --ask) ask="$2"; shift 2 ;;
      --image) image="$2"; shift 2 ;;
      --onstart|--onstart-file) onstart="$2"; shift 2 ;;
      --disk-gb) disk="$2"; shift 2 ;;
      --key|--ssh-pubkey) key="$2"; shift 2 ;;
      --label) label="$2"; shift 2 ;;
      --not-after) VAST_NOT_AFTER="$2"; shift 2 ;;
      --json) VAST_JSON=1; shift ;;
      *) echo "vast-gpu up: unknown flag $1" >&2
         echo "accepted: --ask --image --onstart/--onstart-file --disk-gb --key/--ssh-pubkey --label --not-after --json" >&2
         return 2 ;;
    esac
  done
  if [ -z "$ask" ] || [ -z "$image" ] || [ -z "$onstart" ] || [ -z "$disk" ]; then
    echo "usage: vast-gpu up --ask <offer-id> --image <img> --onstart-file <file> --disk-gb <n> [--key|--ssh-pubkey <public-key-file>]" >&2
    return 2
  fi
  [ -f "$onstart" ] || { echo "vast-gpu: onstart file missing: $onstart" >&2; return 2; }
  key="${key:-${VAST_SSH_PUBKEY:-$HOME/.ssh/id_ed25519.pub}}"
  local canon; canon="$(prepare_key "$key")" || return 2
  echo "identity: $key  $(fingerprint "$canon")" >&2
  label="${label:-forgia-x-$(openssl rand -hex 16)}"
  local d; d="$(evidence_dir allocate)"
  # A quoted "${VAR:+...}" expands to one EMPTY argument when VAR is unset,
  # which the executor rejects as an unknown flag. An array omits it entirely.
  local extra=()
  if [ -n "${VAST_NOT_AFTER:-}" ]; then extra=(--not-after-epoch-ms "$VAST_NOT_AFTER"); fi
  echo "label: $label  (recorded in $ROOT/labels/; it reconciles this rental)"
  echo "evidence: $d" >&2
  local code
  set +e
  "$EXEC" vast allocate --ask "$ask" --label "$label" --image "$image" --disk-gb "$disk" \
    --onstart-file "$onstart" --evidence-dir "$d" --token-file "$(token_file)" ${extra[@]+"${extra[@]}"}
  code=$?
  set -e
  state record "$d" allocate "$code" "$(explain "$code")" "ask $ask label $label" 0 >/dev/null
  # The receipt is the proof of a paid action, so its verdict is printed in
  # full and the document itself is left on disk. Dumping the whole JSON here
  # buried the instance id under the request echo.
  if [ -s "$d/receipt.json" ]; then
    local oc hs nc
    oc="$(state field "$d/receipt.json" outcome)"
    hs="$(state field "$d/receipt.json" http_status)"
    nc="$(state field "$d/receipt.json" new_contract)"
    echo "receipt: outcome=$oc http_status=$hs new_contract=$nc  ($d/receipt.json)"
  fi
  if [ $code -ne 0 ]; then
    [ -s "$d/stderr.txt" ] && cat "$d/stderr.txt" >&2
    echo "allocate exit $code: $(explain "$code")  evidence $d" 
    [ -s "$d/receipt.json" ] && cat "$d/receipt.json"
    return $code
  fi

  # The receipt names the new instance `new_contract`. Searching the evidence
  # for a key called `instance_id` reported failure on every successful
  # allocation and stranded the attach step, so read the documented field.
  local iid; iid="$(state field "$d/receipt.json" new_contract)"
  if [ "$iid" = "MISSING" ]; then iid="$(state field "$d/receipt.json" instance_id)"; fi
  local outcome; outcome="$(state field "$d/receipt.json" outcome)"
  if [ "$iid" = "MISSING" ] || [ "$iid" = "null" ]; then
    echo "vast-gpu: allocate exited 0 but the receipt carries no instance id (outcome=$outcome)." >&2
    echo "vast-gpu: the allocation may still have committed. Reconcile with 'vast-gpu inventory' before renting again." >&2
    echo "vast-gpu: evidence $d" >&2
    return 5
  fi
  echo "instance: $iid  (outcome $outcome)"
  mkdir -p "$ROOT/labels" "$ROOT/instances"
  printf '%s %s %s\n' "$label" "$iid" "$(stamp)" > "$ROOT/labels/$label.txt"
  printf '%s %s\n' "$label" "$iid" > "$d/label.txt"
  bind_key "$iid" "$key"

  local a; a="$(evidence_dir "attach-ssh-$iid")"
  local acode
  set +e; capture_into_dir "$a" vast attach-ssh --instance-id "$iid" --public-key-file "$canon"; acode=$?; set -e
  state record "$a" attach-ssh "$acode" "$(explain "$acode")" "$(head -c 200 "$a/stdout.txt" | tr '\n' ' ')" 0 >/dev/null
  if [ -s "$a/stderr.txt" ]; then cat "$a/stderr.txt" >&2; fi
  local att; att="$(state field "$a/receipt.json" outcome)"
  echo "attach-ssh: $att (exit $acode: $(explain "$acode"))"
  if [ "$att" != "attached" ]; then
    echo "vast-gpu: the provider did not confirm the key. Attach is a mutation: any non-confirmation is reconcile-only, never re-sent blindly." >&2
    echo "vast-gpu: evidence $a" >&2
  fi
  echo "next: vast-gpu where $iid --wait     # status, then the exact ssh command"
  echo "next: vast-gpu ssh-verify $iid       # prove the key landed before spending time on ssh"
  return 0
}

life_where() {
  [ $# -ge 1 ] || { echo "usage: vast-gpu where <instance-id> [--wait] [--timeout s] [--interval s]" >&2; return 2; }
  local iid="$1"; shift
  local want_wait="" timeout=600 interval=15
  while [ $# -gt 0 ]; do
    case "$1" in
      --wait) want_wait=1; shift ;;
      --timeout) timeout="$2"; shift 2 ;;
      --interval) interval="$2"; shift 2 ;;
      *) echo "vast-gpu where: unknown flag $1" >&2; return 2 ;;
    esac
  done
  local deadline=$(( $(date +%s) + timeout )) last_state="" d out
  while :; do
    out="$(life_coords "$iid")" || return $?
    local I_STATUS I_HOST I_PORT
    IFS=$'\t' read -r d I_STATUS I_HOST I_PORT <<<"$out"
    if [ "$I_STATUS" != "$last_state" ]; then
      echo "$(date -u +%H:%M:%SZ) status: $I_STATUS"
      last_state="$I_STATUS"
    fi
    if [ "$I_STATUS" = "running" ]; then
      echo "host: $I_HOST"
      echo "port: $I_PORT"
      life_ssh_line "$iid"
      echo "ready: onstart may still be running; read 'vast-gpu logs $iid --tail 80'"
      return 0
    fi
    if [ -z "$want_wait" ]; then
      echo "ssh is not usable until actual_status is 'running'."
      echo "re-run with --wait to poll inside one call instead of a shell loop."
      return 7
    fi
    [ "$(date +%s)" -lt "$deadline" ] || { echo "vast-gpu: still $I_STATUS after ${timeout}s; evidence $d" >&2; return 7; }
    sleep "$interval"
  done
}

life_ssh() {
  [ $# -ge 1 ] || { echo "usage: vast-gpu ssh <instance-id> [-- command...]" >&2; return 2; }
  local iid="$1"; shift
  if [ "${1:-}" = "--" ]; then shift; fi
  local idkey
  idkey="$(key_for "$iid" || true)"
  [ -n "$idkey" ] || {
    echo "vast-gpu: no identity recorded for $iid. Attach one first; never probe candidate keys." >&2
    return 2
  }
  local out d I_STATUS I_HOST I_PORT
  out="$(life_coords "$iid")" || return 7
  IFS=$'\t' read -r d I_STATUS I_HOST I_PORT <<<"$out"
  [ "$I_STATUS" = "running" ] || { echo "vast-gpu: instance is $I_STATUS, not running; run 'vast-gpu where $iid --wait'" >&2; return 7; }
  if [ $# -eq 0 ]; then
    # No command means "show me the line". Opening an interactive session here
    # is what an agent gets when it only wanted the coordinates.
    echo "ssh -i $idkey -p $I_PORT -o StrictHostKeyChecking=accept-new root@$I_HOST"
    echo "nothing was run. Append '-- <command>' to execute over ssh."
    return 0
  fi
  local code
  set +e
  ssh -i "$idkey" -p "$I_PORT" -o StrictHostKeyChecking=accept-new -o ConnectTimeout=20 root@"$I_HOST" "$@"
  code=$?
  set -e
  if [ $code -eq 255 ]; then
    echo "vast-gpu: ssh failed. Run 'vast-gpu ssh-verify $iid' before trying another key: provider acceptance is not guest presence." >&2
  fi
  return $code
}

life_ssh_verify() {
  [ $# -ge 1 ] || { echo "usage: vast-gpu ssh-verify <instance-id> [--key <pubkey>] [--deadline s]" >&2; return 2; }
  local iid="$1"; shift
  local key="${VAST_SSH_PUBKEY:-$HOME/.ssh/id_ed25519.pub}" deadline=120
  while [ $# -gt 0 ]; do
    case "$1" in
      --key) key="$2"; shift 2 ;;
      --deadline) deadline="$2"; shift 2 ;;
      *) echo "vast-gpu ssh-verify: unknown flag $1" >&2; return 2 ;;
    esac
  done
  local canon fp
  canon="$(prepare_key "$key")" || return 2
  fp="$(fingerprint "$canon")"
  local nonce; nonce="$(openssl rand -hex 16)"
  local d; d="$(evidence_dir "ssh-verify-$iid")"
  echo "identity: $key  $fp"
  echo "evidence: $d" >&2
  local code
  set +e
  capture_into_dir "$d" vast host-key verify --instance-id "$iid" --nonce "$nonce" \
    --client-fingerprint "$fp" --deadline-secs "$deadline" --interval-secs 5
  code=$?
  set -e
  view verify "$d"
  if [ -s "$d/stderr.txt" ]; then cat "$d/stderr.txt" >&2; fi
  state record "$d" ssh-verify "$code" "$(explain_verify "$code")" "$(view verify "$d" 2>/dev/null | head -1 || true)" 0 >/dev/null
  echo "exit $code"
  return $code
}

life_burn() {
  [ $# -ge 1 ] || { echo "usage: vast-gpu burn <instance-id> --yes" >&2; return 2; }
  local iid="$1"; local confirmed=""
  for a in "$@"; do
    if [ "$a" = "--yes" ]; then confirmed=1; fi
  done
  [ -n "$confirmed" ] || { echo "vast-gpu burn: refusing without --yes (destructive, irreversible)" >&2; return 2; }
  local code
  set +e; run "destroy-$iid" vast destroy --instance-id "$iid"; code=$?; set -e
  if [ $code -eq 0 ]; then rm -f "$ROOT/instances/$iid.json"; fi
  echo
  echo "disposal is only proven by absence from a complete inventory:"
  run "inventory-after-$iid" vast inventory
  return $code
}