#!/usr/bin/env bash
#
# detect-agent — Detect which AI agent CLI launched this script.
#
# Checks environment variables and walks the parent process tree to
# identify the originating coding agent.  Designed for agent-agnostic
# tools (skills, scripts, hooks) that need to adjust behaviour based
# on which agent invoked them.
#
# Usage:
#   detect-agent                # print name  (e.g. "pi", "opencode")
#   detect-agent --name         # same as default
#   detect-agent --json         # JSON: {"agent":"pi","detected":true}
#   detect-agent --verbose      # human-readable with debug info
#   detect-agent --list-agents  # list all known agent names
#
# Exit codes:
#   0  agent detected
#   1  unknown / not detected
#
# Supported agents (add your own at the top of each definition map):
#   pi, opencode, aider, claude, codex, cursor, windsurf, continue,
#   github-copilot, goose
#
# Detection order:
#   1. Environment variable signatures (most reliable)
#   2. Walk parent process tree skipping shells
#   3. Check /proc/$PPID/{comm,cmdline} as fallback
#

set -euo pipefail

########################################################################
# Agent definitions
########################################################################

# Each entry: "<env-var>:<match-value>:<agent-name>"
# If match-value is "*", any non-empty value matches.
# If match-value is a literal, the env var must equal it.
AGENT_ENV_SIGS=(
  "PI_CODING_AGENT:true:pi"
  "OPENCODE_CONFIG:*:opencode"
  "OPENCODE_CONFIG_DIR:*:opencode"
  "OPENCODE_API_KEY:*:opencode"
  "CLAUDE_CODE_AGENT_RULE_DISABLED:*:claude"
  "CLAUDE_CODE_SSE_PORT:*:claude"
  "AIDER_DARK_MODE:*:aider"
  "CODEX_API_KEY:*:codex"
  "CURSOR_AGENT_RULE_DISABLED:*:cursor"
  "WINDSURF_AGENT_RULE_DISABLED:*:windsurf"
  "CONTINUE_ON_ERROR:*:continue"
)

# Key = /proc/PID/comm (truncated to 15 chars on Linux).
# Value = canonical agent name.
declare -A AGENT_COMM
AGENT_COMM=(
  [pi]="pi"
  [opencode]="opencode"
  [aider]="aider"
  [claude]="claude"
  [codex]="codex"
  [cursor]="cursor"
  [windsurf]="windsurf"
  [continue]="continue"
  [github-copilot]="github-copilot"
  [copilot]="github-copilot"
  [goose]="goose"
)

# For agents running under an interpreter (node, python, etc.).
# Key = substring to search in cmdline, Value = agent name.
declare -A AGENT_CMDLINE
AGENT_CMDLINE=(
  ["opencode"]="opencode"
  ["/pi "]="pi"
  ["/aider"]="aider"
  ["/claude"]="claude"
  ["/codex"]="codex"
  ["cursor"]="cursor"
  ["windsurf"]="windsurf"
  ["continue.dev"]="continue"
  ["github-copilot"]="github-copilot"
  ["goose"]="goose"
)

# Shells to skip when walking ancestors.
SHELL_COMMS=(
  bash zsh fish sh dash ksh mksh posh ash tcsh csh
  tmux:server tmux screen
  sudo doas
)

SHELL_CMDLINE_SUBSTRINGS=(
  "/bash" "/zsh" "/fish" "/sh" "/dash" "/ksh"
)

########################################################################
# Helpers
########################################################################

comm_of() {
  local pid="$1"
  cat "/proc/${pid}/comm" 2>/dev/null || true
}

cmdline_of() {
  local pid="$1"
  tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true
}

is_shell() {
  local pid="$1"
  local c; c="$(comm_of "$pid")" || true
  [[ -z "$c" ]] && return 1

  for s in "${SHELL_COMMS[@]}"; do
    [[ "$c" == "$s" ]] && return 0
  done

  local cl; cl="$(cmdline_of "$pid")" || true
  for s in "${SHELL_CMDLINE_SUBSTRINGS[@]}"; do
    [[ "$cl" == *"$s"* ]] && return 0
  done

  return 1
}

walk_to_agent_comm() {
  local pid="$1"
  local c
  while [[ "$pid" -gt 1 ]]; do
    c="$(comm_of "$pid")" || true
    [[ -z "$c" ]] && break
    if ! is_shell "$pid"; then
      echo "$c"
      return 0
    fi
    pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
    [[ -z "$pid" ]] && break
  done
  return 1
}

walk_to_agent_cmdline() {
  local pid="$1"
  local cl
  while [[ "$pid" -gt 1 ]]; do
    if ! is_shell "$pid"; then
      cl="$(cmdline_of "$pid")" || true
      if [[ -n "$cl" ]]; then
        echo "$cl"
        return 0
      fi
    fi
    pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
    [[ -z "$pid" ]] && break
  done
  return 1
}

check_env_sigs() {
  local var val agent
  for sig in "${AGENT_ENV_SIGS[@]}"; do
    IFS=':' read -r var val agent <<< "$sig"
    if [[ "$val" == "*" ]]; then
      if [[ -n "${!var:-}" ]]; then
        echo "$agent"
        return 0
      fi
    else
      if [[ "${!var:-}" == "$val" ]]; then
        echo "$agent"
        return 0
      fi
    fi
  done
  return 1
}

match_cmdline() {
  local cl="$1"
  local needle agent
  for needle in "${!AGENT_CMDLINE[@]}"; do
    agent="${AGENT_CMDLINE[$needle]}"
    if [[ "$cl" == *"$needle"* ]]; then
      echo "$agent"
      return 0
    fi
  done
  return 1
}

match_comm() {
  local c="$1"
  local comm_name
  for comm_name in "${!AGENT_COMM[@]}"; do
    if [[ "$c" == "$comm_name" ]]; then
      echo "${AGENT_COMM[$comm_name]}"
      return 0
    fi
  done
  return 1
}

########################################################################
# Main detection logic
########################################################################

detect() {
  local agent=""

  # Phase 1 — environment variables (most reliable)
  agent="$(check_env_sigs)" || true
  [[ -n "$agent" ]] && echo "$agent" && return 0

  # Phase 2 — walk ancestors, check comm
  local ancestor_comm
  ancestor_comm="$(walk_to_agent_comm "$PPID")" || true
  if [[ -n "$ancestor_comm" ]]; then
    agent="$(match_comm "$ancestor_comm")" || true
    [[ -n "$agent" ]] && echo "$agent" && return 0

    # comm matched but not in our map; try its cmdline
    local ancestor_pid="$PPID"
    while [[ "$ancestor_pid" -gt 1 ]]; do
      local ac; ac="$(comm_of "$ancestor_pid")" || true
      if [[ "$ac" == "$ancestor_comm" ]]; then
        local ancestor_cl
        ancestor_cl="$(cmdline_of "$ancestor_pid")" || true
        if [[ -n "$ancestor_cl" ]]; then
          agent="$(match_cmdline "$ancestor_cl")" || true
          [[ -n "$agent" ]] && echo "$agent" && return 0
        fi
        break
      fi
      ancestor_pid="$(awk '/^PPid:/{print $2}' "/proc/${ancestor_pid}/status" 2>/dev/null || true)"
    done
  fi

  # Phase 3 — walk ancestors, check cmdline (catches node-based agents)
  local ancestor_cl
  ancestor_cl="$(walk_to_agent_cmdline "$PPID")" || true
  if [[ -n "$ancestor_cl" ]]; then
    agent="$(match_cmdline "$ancestor_cl")" || true
    [[ -n "$agent" ]] && echo "$agent" && return 0
  fi

  # Phase 4 — fallback: check immediate PPID directly
  local ppid_comm; ppid_comm="$(comm_of "$PPID")" || true
  agent="$(match_comm "$ppid_comm")" || true
  [[ -n "$agent" ]] && echo "$agent" && return 0

  local ppid_cl; ppid_cl="$(cmdline_of "$PPID")" || true
  agent="$(match_cmdline "$ppid_cl")" || true
  [[ -n "$agent" ]] && echo "$agent" && return 0

  return 1
}

########################################################################
# Output
########################################################################

mode="${1:---name}"

case "$mode" in
  --list-agents)
    echo "Known agents (by env-var / process-name):"
    echo ""
    echo "Environment variable signatures:"
    for sig in "${AGENT_ENV_SIGS[@]}"; do
      IFS=':' read -r var val agent <<< "$sig"
      printf "  %-12s  %-8s  %s=%s\n" "$agent" "(env)" "$var" "$val"
    done
    echo ""
    echo "Process comm signatures:"
    for comm_name in "${!AGENT_COMM[@]}"; do
      printf "  %-12s  %-8s  %s\n" "${AGENT_COMM[$comm_name]}" "(comm)" "$comm_name"
    done
    echo ""
    echo "Cmdline substring signatures:"
    for needle in "${!AGENT_CMDLINE[@]}"; do
      printf "  %-12s  %-8s  *%s*\n" "${AGENT_CMDLINE[$needle]}" "(cmdline)" "$needle"
    done
    ;;

  --json)
    agent="$(detect)" || agent="null"
    if [[ "$agent" != "null" ]]; then
      printf '{"agent":"%s","detected":true}\n' "$agent"
    else
      printf '{"agent":null,"detected":false}\n'
      exit 1
    fi
    ;;

  --verbose)
    echo "=== Agent Detection ==="
    agent="$(detect)" || true
    if [[ -n "$agent" ]]; then
      echo "Detected agent: $agent"
    else
      echo "Detected agent: (unknown)"
    fi
    echo ""

    echo "Relevant environment variables:"
    for sig in "${AGENT_ENV_SIGS[@]}"; do
      IFS=':' read -r var val _ <<< "$sig"
      # Skip entries that aren't real variable names (e.g. patterns with *).
      [[ "$var" == *\** ]] && continue
      if [[ -n "${!var:-}" ]]; then
        echo "  $var=${!var}"
      fi
    done
    [[ -z "${PI_CODING_AGENT:-}" ]] && echo "  PI_CODING_AGENT=<unset>"
    echo ""

    echo "Process tree (up from PPID=$PPID):"
    _dbg_pid="$PPID"
    _dbg_depth=0
    while [[ "$_dbg_pid" -gt 1 && "$_dbg_depth" -lt 15 ]]; do
      _dbg_c="$(comm_of "$_dbg_pid")" || true
      _dbg_cl="$(cmdline_of "$_dbg_pid")" || true
      if [[ -n "$_dbg_c" ]]; then
        printf "  [%d] %s" "$_dbg_depth" "$_dbg_c"
        if [[ -n "$_dbg_cl" ]]; then
          printf "  <- %s" "${_dbg_cl:0:100}"
        fi
        if [[ -n "$agent" ]]; then
          _dbg_mc="$(match_comm "$_dbg_c")" || true
          if [[ "$_dbg_mc" == "$agent" ]]; then
            printf "  <-- DETECTED"
          fi
        fi
        echo ""
      fi
      _dbg_pid="$(awk '/^PPid:/{print $2}' "/proc/${_dbg_pid}/status" 2>/dev/null || true)"
      _dbg_depth=$((_dbg_depth + 1))
    done
    ;;

  --name|*)
    agent="$(detect)" || agent=""
    if [[ -n "$agent" ]]; then
      echo "$agent"
    else
      echo "unknown"
      exit 1
    fi
    ;;
esac
