#!/usr/bin/env bash
# Read-only discovery for the Overdeck buildbox/K3s migration.
# It deliberately never reads Kubernetes Secret data, K3s token contents,
# shell environments, or systemd Environment= values.
set -u -o pipefail
umask 077

usage() {
  cat <<'USAGE'
Usage: tools/k3s/phase0-discover.sh [options]

Options:
  --repo-root PATH       Overdeck checkout/worktree (default: inferred)
  --output-dir PATH      Receipt directory (default: ./overdeck-k3s-phase0-receipt-<UTC>)
  --kubeconfig PATH      Kubeconfig (default: ~/.kube/config-buildboxes)
  --server HOST          K3s server SSH alias (default: debian3)
  --hosts CSV            Host aliases; default is buildbox-hosts.json
  --ssh-timeout SEC      Per-connection timeout (default: 12)
  --local-only           Do not contact SSH/Tailscale peers
  --skip-kubectl         Do not query the Kubernetes API
  --quick                Skip deeper fleet/buildbox audits
  --no-archive           Do not create the final .tar.gz
  -h, --help             Show this help

The script is read-only. Non-zero probe commands are recorded rather than aborting the
receipt, because unreachable nodes are part of the state we need to observe.
USAGE
}

die() { printf 'phase0-discover: %s\n' "$*" >&2; exit 2; }
log() { printf '[phase0] %s\n' "$*" >&2; }

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
DEFAULT_REPO=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
REPO_ROOT=$DEFAULT_REPO
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
OUTPUT_DIR=""
KUBECONFIG_PATH=${BUILD_REMOTE_K3S_KUBECONFIG:-"$HOME/.kube/config-buildboxes"}
SERVER=debian3
HOSTS_CSV=""
SSH_TIMEOUT=12
LOCAL_ONLY=0
SKIP_KUBECTL=0
DEEP=1
MAKE_ARCHIVE=1

while (($#)); do
  case "$1" in
    --repo-root) (($# >= 2)) || die "--repo-root requires a value"; REPO_ROOT=$2; shift 2 ;;
    --output-dir) (($# >= 2)) || die "--output-dir requires a value"; OUTPUT_DIR=$2; shift 2 ;;
    --kubeconfig) (($# >= 2)) || die "--kubeconfig requires a value"; KUBECONFIG_PATH=$2; shift 2 ;;
    --server) (($# >= 2)) || die "--server requires a value"; SERVER=$2; shift 2 ;;
    --hosts) (($# >= 2)) || die "--hosts requires a value"; HOSTS_CSV=$2; shift 2 ;;
    --ssh-timeout) (($# >= 2)) || die "--ssh-timeout requires a value"; SSH_TIMEOUT=$2; shift 2 ;;
    --local-only) LOCAL_ONLY=1; shift ;;
    --skip-kubectl) SKIP_KUBECTL=1; shift ;;
    --quick) DEEP=0; shift ;;
    --no-archive) MAKE_ARCHIVE=0; shift ;;
    -h|--help) usage; exit 0 ;;
    *) die "unknown option: $1" ;;
  esac
done

[[ "$SSH_TIMEOUT" =~ ^[1-9][0-9]*$ ]] || die "--ssh-timeout must be a positive integer"
REPO_ROOT=$(CDPATH= cd -- "$REPO_ROOT" 2>/dev/null && pwd) || die "repo root does not exist: $REPO_ROOT"
[[ -d "$REPO_ROOT/.git" || -f "$REPO_ROOT/.git" ]] || die "not a Git checkout/worktree: $REPO_ROOT"
[[ -f "$REPO_ROOT/modules/fleet/fleet.json" ]] || die "not an Overdeck checkout: $REPO_ROOT"

if [[ -z "$OUTPUT_DIR" ]]; then
  OUTPUT_DIR="$PWD/overdeck-k3s-phase0-receipt-$STAMP"
fi
case "$OUTPUT_DIR" in
  /*) ;;
  *) OUTPUT_DIR="$PWD/$OUTPUT_DIR" ;;
esac
[[ ! -e "$OUTPUT_DIR" ]] || die "output already exists: $OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR/raw/local" "$OUTPUT_DIR/raw/kubernetes" "$OUTPUT_DIR/raw/hosts" "$OUTPUT_DIR/raw/fleet"
RESULTS_TSV="$OUTPUT_DIR/results.tsv"
: >"$RESULTS_TSV"

safe_name() {
  printf '%s' "$1" | tr -cs 'A-Za-z0-9._-' '_'
}

record_result() {
  local group=$1 name=$2 rc=$3 rel=$4
  printf '%s\t%s\t%s\t%s\n' "$group" "$name" "$rc" "$rel" >>"$RESULTS_TSV"
}

run_capture() {
  local group=$1 name=$2; shift 2
  local safe_group safe_file rel file rc
  safe_group=$(safe_name "$group")
  safe_file=$(safe_name "$name")
  mkdir -p "$OUTPUT_DIR/raw/$safe_group"
  rel="raw/$safe_group/$safe_file.txt"
  file="$OUTPUT_DIR/$rel"
  (
    printf 'command:'
    printf ' %q' "$@"
    printf '\nstarted_utc: %s\n\n' "$(date -u +%FT%TZ)"
    "$@"
    rc=$?
    printf '\nexit_code: %s\n' "$rc"
    exit "$rc"
  ) >"$file" 2>&1
  rc=$?
  record_result "$group" "$name" "$rc" "$rel"
  return 0
}

run_bash() {
  local group=$1 name=$2 script=$3
  run_capture "$group" "$name" bash -o pipefail -c "$script"
}

have() { command -v "$1" >/dev/null 2>&1; }

host_inventory_file="$REPO_ROOT/modules/workstation/claude/buildbox-hosts.json"
if [[ -z "$HOSTS_CSV" ]]; then
  HOSTS_CSV=$(python3 - "$host_inventory_file" <<'PY'
import json, sys
with open(sys.argv[1], encoding="utf-8") as fh:
    doc=json.load(fh)
print(",".join(str(item["name"]) for item in doc.get("hosts", [])))
PY
) || die "cannot read host registry"
fi
IFS=',' read -r -a HOSTS <<<"$HOSTS_CSV"
((${#HOSTS[@]} > 0)) || die "no hosts selected"

python3 - "$host_inventory_file" >"$OUTPUT_DIR/host-inventory.tsv" <<'PY'
import json, sys
with open(sys.argv[1], encoding="utf-8") as fh:
    doc=json.load(fh)
for h in doc.get("hosts", []):
    access=h.get("access") or {}
    ip=access.get("tailscale_ip") or {}
    ts=access.get("tailscale_ssh") or {}
    values=[h.get("name",""), h.get("state",""), ip.get("host",""), str(ip.get("port","")), ip.get("user",""), ts.get("host",""), str(ts.get("port","")), ts.get("user","")]
    print("\t".join(str(v) for v in values))
PY

cat >"$OUTPUT_DIR/README.txt" <<EOF_README
Overdeck K3s Phase 0 discovery receipt
Generated: $(date -u +%FT%TZ)
Repository: $REPO_ROOT
Kubeconfig: $KUBECONFIG_PATH
Server alias: $SERVER
Hosts: $HOSTS_CSV
Mode: local_only=$LOCAL_ONLY skip_kubectl=$SKIP_KUBECTL deep=$DEEP

This receipt intentionally excludes Kubernetes Secret data, K3s token contents,
process environments, and systemd Environment= values.
EOF_README

log "collecting local repository and host state"
run_capture local identity bash -c 'printf "user=%s\nuid=%s\nhost=%s\nhome=%s\n" "$(id -un)" "$(id -u)" "$(hostname -f 2>/dev/null || hostname)" "$HOME"; uname -a; date -u +%FT%TZ'
run_capture local git-state /usr/bin/git -C "$REPO_ROOT" status --short --branch
run_capture local git-head /usr/bin/git -C "$REPO_ROOT" log -1 --decorate=short --format=fuller
run_capture local git-worktrees /usr/bin/git -C "$REPO_ROOT" worktree list --porcelain
run_capture local tracked-config-hashes bash -c 'cd "$1" && sha256sum modules/fleet/fleet.json modules/workstation/claude/buildbox-hosts.json modules/workstation/claude/build-remote.json docs/plans/2026-08-10-factory-k3s.md docs/specs/2026-08-08-k3s-scheduler-design.md' _ "$REPO_ROOT"
run_capture local config-metadata bash -c 'for f in "$HOME/.claude/build-remote.json" "$HOME/.kube/config-buildboxes"; do if [ -e "$f" ]; then stat -c "%a %U:%G %s %y %n" "$f"; sha256sum "$f"; else echo "missing $f"; fi; done'
run_capture local resource-snapshot bash -c 'uptime; printf "\n-- memory --\n"; free -h; printf "\n-- disks --\n"; df -hT / "$HOME"; printf "\n-- mounts --\n"; findmnt -rn -o TARGET,SOURCE,FSTYPE,OPTIONS / /var/lib/buildbox 2>/dev/null || true'

if have tailscale; then
  run_capture local tailscale-version tailscale version
  run_capture local tailscale-status tailscale status --json
  run_capture local tailscale-ip tailscale ip
else
  run_bash local tailscale-missing 'echo "tailscale command not found"; exit 127'
fi

if [[ -x "$REPO_ROOT/bin/deckctl" ]]; then
  run_capture fleet status "$REPO_ROOT/bin/deckctl" fleet status
  if ((DEEP)); then
    run_capture fleet audit "$REPO_ROOT/bin/deckctl" fleet audit
  fi
fi

if ((SKIP_KUBECTL == 0)); then
  if have kubectl; then
    K=(kubectl --kubeconfig "$KUBECONFIG_PATH" --request-timeout=12s)
    run_capture kubernetes client-version kubectl version --client=true -o yaml
    run_capture kubernetes context "${K[@]}" config current-context
    run_capture kubernetes api-server "${K[@]}" config view --minify -o 'jsonpath={.clusters[0].cluster.server}{"\n"}'
    run_capture kubernetes version "${K[@]}" version -o yaml
    run_capture kubernetes readyz "${K[@]}" get --raw='/readyz?verbose'
    run_capture kubernetes nodes-wide "${K[@]}" get nodes -o wide --show-labels
    run_capture kubernetes nodes-json "${K[@]}" get nodes -o json
    run_capture kubernetes namespaces "${K[@]}" get namespaces --show-labels
    run_capture kubernetes workloads "${K[@]}" get pods,jobs,deployments,statefulsets,daemonsets -A -o wide
    run_capture kubernetes storage "${K[@]}" get storageclass,persistentvolume,persistentvolumeclaim -A -o wide
    run_capture kubernetes policy "${K[@]}" get resourcequota,limitrange,networkpolicy,poddisruptionbudget -A -o wide
    run_capture kubernetes admission "${K[@]}" get validatingadmissionpolicies,validatingadmissionpolicybindings,mutatingwebhookconfigurations,validatingwebhookconfigurations -o wide
    run_capture kubernetes rbac "${K[@]}" get serviceaccounts,roles,rolebindings -A -o wide
    run_capture kubernetes cluster-rbac "${K[@]}" get clusterroles,clusterrolebindings -o name
    run_capture kubernetes secret-metadata "${K[@]}" get secrets -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.type,CREATED:.metadata.creationTimestamp'
    run_capture kubernetes events "${K[@]}" get events -A --sort-by=.metadata.creationTimestamp
    run_capture kubernetes metrics "${K[@]}" top nodes
  else
    run_bash kubernetes kubectl-missing 'echo "kubectl command not found"; exit 127'
  fi
fi

remote_probe='set -u
printf "hostname=%s\n" "$(hostname -f 2>/dev/null || hostname)"
printf "machine_id="; cat /etc/machine-id 2>/dev/null || true
printf "boot_id="; cat /proc/sys/kernel/random/boot_id 2>/dev/null || true
printf "kernel="; uname -srmo
printf "uptime="; uptime -p 2>/dev/null || uptime
printf "tailscale_ip="; tailscale ip -4 2>/dev/null | paste -sd, - || true
printf "\n-- versions --\n"
tailscale version 2>/dev/null | head -n 2 || true
k3s --version 2>/dev/null || true
printf "\n-- services --\n"
for unit in tailscaled.service ssh.service buildbox-rescue-sshd.socket buildbox-tailscale-ssh.timer k3s.service k3s-agent.service; do
  printf "%s=" "$unit"; systemctl is-active "$unit" 2>/dev/null || true
done
printf "\n-- selected service properties --\n"
for unit in k3s.service k3s-agent.service; do
  systemctl show "$unit" -p LoadState -p ActiveState -p SubState -p UnitFileState -p FragmentPath -p ExecMainStatus --no-pager 2>/dev/null || true
done
printf "\n-- k3s config metadata (contents intentionally omitted) --\n"
sudo -n sh -c '\''for f in /etc/rancher/k3s/config.yaml /etc/rancher/k3s/config.yaml.d/*.yaml; do [ -e "$f" ] || continue; stat -c "%a %U:%G %s %y %n" "$f"; sha256sum "$f"; done; for f in /var/lib/rancher/k3s/server/token /var/lib/rancher/k3s/server/agent-token; do [ -e "$f" ] || continue; stat -Lc "%a %U:%G %s %y %n" "$f"; done'\'' 2>/dev/null || true
printf "\n-- datastore/backup metadata --\n"
sudo -n sh -c '\''du -sh /var/lib/rancher/k3s/server/db 2>/dev/null || true; find /var/lib/rancher/k3s/server/db -maxdepth 2 -type f -printf "%TY-%Tm-%TdT%TH:%TM:%TSZ %s %p\n" 2>/dev/null | sort | tail -n 80'\'' 2>/dev/null || true
printf "\n-- resources --\n"
free -h 2>/dev/null || true
df -hT / /var/lib/buildbox /var/lib/rancher/k3s 2>/dev/null || true
findmnt -rn -o TARGET,SOURCE,FSTYPE,OPTIONS / /var/lib/buildbox /var/lib/rancher/k3s 2>/dev/null || true
printf "\n-- listeners --\n"
sudo -n ss -ltnp 2>/dev/null | awk '\''NR==1 || $4 ~ /:(22|2222|2223|6443|10250)$/'\'' || ss -ltn 2>/dev/null | awk '\''NR==1 || $4 ~ /:(22|2222|2223|6443|10250)$/'\'' || true
'

if ((LOCAL_ONLY == 0)); then
  log "collecting read-only peer and SSH-door state"
  while IFS=$'\t' read -r name declared_state ts_ip ts_port ts_user ts_dns ts_ssh_port ts_ssh_user; do
    selected=0
    for wanted in "${HOSTS[@]}"; do [[ "$wanted" == "$name" ]] && selected=1; done
    ((selected)) || continue
    run_bash hosts "$name-registry" "printf '%s\n' 'declared_state=$declared_state' 'tailscale_ip=$ts_ip' 'tailscale_ssh=$ts_dns'"
    if have tailscale && [[ -n "$ts_dns" ]]; then
      run_capture hosts "$name-tailscale-ping" timeout "${SSH_TIMEOUT}s" tailscale ping --timeout="${SSH_TIMEOUT}s" "$ts_dns"
    fi
    if [[ -n "$ts_ip" ]]; then
      for port in 22 2222 2223 6443; do
        run_bash hosts "$name-port-$port" "timeout ${SSH_TIMEOUT}s bash -c '</dev/tcp/$ts_ip/$port'"
      done
    fi
    run_capture hosts "$name-ssh-alias" timeout "${SSH_TIMEOUT}s" ssh -o BatchMode=yes -o ConnectTimeout="$SSH_TIMEOUT" "$name" "$remote_probe"
    if ((DEEP)) && [[ -x "$REPO_ROOT/modules/buildbox/bin/buildbox" ]]; then
      run_capture fleet "$name-buildbox-audit" timeout 180s "$REPO_ROOT/modules/buildbox/bin/buildbox" audit "$name"
    fi
    if have tailscale && [[ -n "$ts_dns" && -n "$ts_ssh_user" ]]; then
      run_capture hosts "$name-tailscale-ssh" timeout "${SSH_TIMEOUT}s" tailscale ssh "$ts_ssh_user@$ts_dns" 'printf "tailscale_ssh_ok=%s\n" "$(hostname)"'
    fi
  done <"$OUTPUT_DIR/host-inventory.tsv"

  # Cross-host API reachability is an authentication-independent TCP/TLS check.
  server_ip=$(awk -F '\t' -v s="$SERVER" '$1==s {print $3}' "$OUTPUT_DIR/host-inventory.tsv" | head -n1)
  if [[ -n "$server_ip" ]]; then
    for host in "${HOSTS[@]}"; do
      [[ "$host" == "$SERVER" ]] && continue
      run_capture hosts "$host-to-k3s-api" timeout "$((SSH_TIMEOUT + 5))s" ssh -o BatchMode=yes -o ConnectTimeout="$SSH_TIMEOUT" "$host" \
        "code=\$(curl -ksS -o /dev/null -w '%{http_code}' --connect-timeout $SSH_TIMEOUT https://$server_ip:6443/healthz 2>/dev/null || true); printf 'http_code=%s\\n' \"\$code\"; test \"\$code\" = 401 -o \"\$code\" = 200"
    done
  fi
fi

log "building machine-readable receipt"
python3 - "$RESULTS_TSV" "$OUTPUT_DIR/receipt.json" "$REPO_ROOT" "$KUBECONFIG_PATH" "$SERVER" "$HOSTS_CSV" <<'PY'
import json, os, sys
from datetime import datetime, timezone
results=[]
with open(sys.argv[1], encoding="utf-8") as fh:
    for line in fh:
        group,name,rc,path=line.rstrip("\n").split("\t",3)
        results.append({"group":group,"name":name,"exit_code":int(rc),"path":path})
out={
  "schema_version":1,
  "generated_utc":datetime.now(timezone.utc).isoformat(),
  "repository":os.path.realpath(sys.argv[3]),
  "kubeconfig":os.path.realpath(os.path.expanduser(sys.argv[4])),
  "server":sys.argv[5],
  "hosts":[x for x in sys.argv[6].split(",") if x],
  "security":{"secret_data_collected":False,"k3s_token_contents_collected":False,"process_environments_collected":False},
  "results":results,
  "counts":{"commands":len(results),"zero":sum(r["exit_code"]==0 for r in results),"nonzero":sum(r["exit_code"]!=0 for r in results)},
}
with open(sys.argv[2],"w",encoding="utf-8") as fh:
    json.dump(out,fh,indent=2,sort_keys=True); fh.write("\n")
PY

python3 - "$OUTPUT_DIR/receipt.json" >"$OUTPUT_DIR/SUMMARY.txt" <<'PY'
import json, sys
r=json.load(open(sys.argv[1],encoding="utf-8"))
print("Overdeck K3s Phase 0 receipt")
print("Generated:",r["generated_utc"])
print("Repository:",r["repository"])
print("Server:",r["server"])
print("Hosts:",", ".join(r["hosts"]))
print("Commands:",r["counts"]["commands"],"zero:",r["counts"]["zero"],"nonzero:",r["counts"]["nonzero"])
print("\nNon-zero probes (inspect; not all indicate defects):")
for item in r["results"]:
    if item["exit_code"]:
        print(f"- {item['group']}/{item['name']} rc={item['exit_code']} -> {item['path']}")
PY

(
  cd "$OUTPUT_DIR"
  find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum >SHA256SUMS
)

archive=""
if ((MAKE_ARCHIVE)); then
  archive="${OUTPUT_DIR%/}.tar.gz"
  tar -C "$(dirname "$OUTPUT_DIR")" -czf "$archive" "$(basename "$OUTPUT_DIR")"
fi

printf '\nReceipt directory: %s\n' "$OUTPUT_DIR"
[[ -z "$archive" ]] || printf 'Upload this archive for the next phase: %s\n' "$archive"
printf 'Non-zero probes are inventory signals; inspect SUMMARY.txt and raw outputs.\n'
