# deckctl agents — enumerate and stop every agent workload, laptop and buildboxes.
# The one red button: `deckctl agents stop --all`.
#
# Local agent work runs as user scopes (confine-agent-*, confine-build-*,
# v2-child-dispatch-*). Buildbox agent work lives under the user-manager slices
# agent.slice / agent-seat.slice / build.slice, and under seat users' (ods-*)
# entire user trees as container scopes. Remote stop writes 1 to the scope's
# cgroup.kill: the kernel kills the whole subtree, no process cooperation needed.

AGENTS_LOCAL_UNIT_RE='^(confine-agent|confine-build|v2-child-dispatch)-.*\.scope$'
AGENTS_REMOTE_PATH_RE='/(agent\.slice|agent-seat\.slice|build\.slice)/[^/]+\.scope$|/user-[0-9]+\.slice/user@[0-9]+\.service/.*/(libpod-[^/]+|run-[^/]+)\.scope$'

agents_filter_local() { # stdin: unit names, one per line
  grep -E "$AGENTS_LOCAL_UNIT_RE" || true
}

# A controlling tty on any process in the scope means a person is attached to it. The
# owner's interactive session is allowed to run locally and is never listed as agent
# workload nor stopped, so a scope is treated as a dispatched seat ONLY on positive
# evidence: its procs are readable and not one of them holds a tty. Unreadable or
# vanished resolves to human, never to a kill.
agents_procs_is_agent() { # $1 = cgroup.procs path
  local procs="$1" pid stat rest fields
  [[ -r "$procs" ]] || return 1
  while IFS= read -r pid; do
    [[ "$pid" =~ ^[0-9]+$ ]] || continue
    IFS= read -r stat < "/proc/$pid/stat" 2>/dev/null || continue
    rest=${stat##*') '}
    fields=($rest)
    [[ "${fields[4]:-0}" != 0 ]] && return 1
  done < "$procs"
  return 0
}

agents_scope_is_agent() { # $1 = user scope unit name
  local cg
  cg=$(systemctl --user show -p ControlGroup --value "$1" 2>/dev/null) || return 1
  [[ -n "$cg" ]] || return 1
  agents_procs_is_agent "${AGENTS_CGROUP_ROOT:-/sys/fs/cgroup}${cg}/cgroup.procs"
}

agents_classify_local() { # prints "agent <unit>" / "human <unit>"
  local u
  while IFS= read -r u; do
    if agents_scope_is_agent "$u"; then echo "agent $u"; else echo "human $u"; fi
  done < <(systemctl --user list-units --type=scope --plain --no-legend 2>/dev/null \
    | awk '{print $1}' | agents_filter_local)
}

agents_filter_remote_paths() { # stdin: cgroup dir paths; $1: interactive uid to exempt from the user-tree match
  local exempt_uid="$1"
  grep -E "$AGENTS_REMOTE_PATH_RE" | grep -v "/user-${exempt_uid}\.slice/user@${exempt_uid}\.service/[^/]*\.scope" || true
}

agents_fleet_hosts() { # prints one ssh host_ref per remote agent-runtime node
  ( cd "$DECKCTL_ROOT" && node --input-type=module -e '
import { loadFleet } from "./lib/fleet/loader.mjs";
for (const node of loadFleet().nodesByRole("agent-runtime")) {
  if (node.transport === "ssh") process.stdout.write(`${node.host_ref}\n`);
}
' )
}

agents_config() { # prints: port user identity host...
  local cfg="${BUILDBOX_CONFIG:-$HOME/.claude/build-remote.json}" hosts
  [[ -r "$cfg" ]] || die "agents: buildbox config missing: $cfg"
  hosts=$(agents_fleet_hosts) || die "agents: fleet declaration unusable — refusing to report an empty host list"
  [[ -n "$hosts" ]] || die "agents: fleet declares no remote agent-runtime node"
  python3 -c '
import json,sys
c=json.load(open(sys.argv[1]))
print(c.get("port",22)); print(c.get("ssh_user","user")); print(c.get("identity_file",""))
' "$cfg"
  printf '%s\n' "$hosts"
}

agents_ssh() { # $1=port $2=user $3=identity $4=host $5...=command
  local port="$1" user="$2" ident="$3" host="$4"; shift 4
  ident="${ident/#\~/$HOME}"
  ssh -F /dev/null -p "$port" ${ident:+-i "$ident"} -o BatchMode=yes -o ConnectTimeout=8 "$user@$host" "$@"
}

agents_list_local() {
  agents_classify_local | sed -n 's/^agent //p'
}

agents_list_remote() { # $1..4 = ssh params; prints cgroup paths
  local uid
  uid=$(agents_ssh "$@" 'id -u' 2>/dev/null) || { echo "agents: WARN: $4 unreachable" >&2; return 0; }
  agents_ssh "$@" 'find /sys/fs/cgroup/user.slice -mindepth 2 -maxdepth 8 -type d -name "*.scope" 2>/dev/null' \
    | agents_filter_remote_paths "$uid"
}

agents_stop_remote_path() { # $1..4 = ssh params, $5 = cgroup path
  local path="$5"
  case "$path" in *[!A-Za-z0-9._@:/+-]*) die "agents: refuse kill path with unsafe characters: $path" ;; esac
  case "$path" in /sys/fs/cgroup/user.slice/*) ;; *) die "agents: refuse kill outside user.slice: $path" ;; esac
  printf '%s\0' "$path" | agents_ssh "${@:1:4}" \
    "sudo -n xargs -0 sh -c 'case \"\$1\" in /sys/fs/cgroup/user.slice/*) printf 1 > \"\$1\"/cgroup.kill;; *) exit 2;; esac' _"
}

cmd_agents() {
  local sub="${1:-}"; shift || true
  case "$sub" in
    list|stop) ;;
    *) echo "usage: deckctl agents list | agents stop --all" >&2; return 2 ;;
  esac

  local port user ident hosts=() config
  config=$(agents_config) || exit 1
  { read -r port; read -r user; read -r ident; while read -r h; do hosts+=("$h"); done; } <<<"$config"

  if [[ "$sub" == "list" ]]; then
    local u p n=0 c=0 summary classified exempt
    classified=$(agents_classify_local)
    exempt=$(printf '%s\n' "$classified" | grep -c '^human ' || true)
    while IFS= read -r u; do
      [[ -n "$u" ]] || continue
      echo "local $u"; c=$((c+1))
    done < <(printf '%s\n' "$classified" | sed -n 's/^agent //p')
    summary="local: $c"; n=$c
    for h in "${hosts[@]}"; do
      c=0
      while IFS= read -r p; do echo "$h ${p#/sys/fs/cgroup/}"; c=$((c+1)); done \
        < <(agents_list_remote "$port" "$user" "$ident" "$h")
      summary+=" | $h: $c"; n=$((n+c))
    done
    # The invariant line, in the shape the systray/deck badge renders: local must read 0.
    echo "$summary"
    echo "total: $n agent workloads"
    echo "exempt: $exempt interactive session(s) — tty attached, never listed or stopped"
    return 0
  fi

  [[ "${1:-}" == "--all" ]] || { echo "usage: deckctl agents stop --all" >&2; return 2; }
  local stopped=0 failed=0
  while IFS= read -r u; do
    if systemctl --user stop "$u" 2>/dev/null; then echo "STOP local $u"; stopped=$((stopped+1))
    else echo "FAIL local $u" >&2; failed=$((failed+1)); fi
  done < <(agents_list_local)
  for h in "${hosts[@]}"; do
    while IFS= read -r p; do
      if agents_stop_remote_path "$port" "$user" "$ident" "$h" "$p"; then echo "STOP $h ${p#/sys/fs/cgroup/}"; stopped=$((stopped+1))
      else echo "FAIL $h ${p#/sys/fs/cgroup/}" >&2; failed=$((failed+1)); fi
    done < <(agents_list_remote "$port" "$user" "$ident" "$h")
  done
  echo "stopped: $stopped failed: $failed"
  [[ "$failed" -eq 0 ]]
}
