#!/usr/bin/env bash
# Read-only classifier for 110 SSH publickey authentication stalls.
#
# Run from an operator host. It does not write remote state, does not read
# authorized_keys contents, and does not ask for passwords.

set -uo pipefail

HOST="${HOST:-192.168.0.110}"
PORT="${PORT:-22}"
NODE_EXPORTER_PORT="${NODE_EXPORTER_PORT:-9100}"
CONNECT_TIMEOUT_SECONDS="${CONNECT_TIMEOUT_SECONDS:-4}"
SSH_ATTEMPT_TIMEOUT_SECONDS="${SSH_ATTEMPT_TIMEOUT_SECONDS:-8}"
SSH_COMMAND_PATH_TIMEOUT_SECONDS="${SSH_COMMAND_PATH_TIMEOUT_SECONDS:-20}"
NODE_EXPORTER_TIMEOUT_SECONDS="${NODE_EXPORTER_TIMEOUT_SECONDS:-8}"
USERS=(${USERS:-wooo root git ollama})
COMMAND_PATH_USER="${COMMAND_PATH_USER:-wooo}"

tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/awoooi-110-ssh-auth.XXXXXX")"
trap 'rm -rf "$tmp_dir"' EXIT

ssh_base_opts=(
  -vv
  -p "$PORT"
  -o ConnectTimeout="$CONNECT_TIMEOUT_SECONDS"
  -o ConnectionAttempts=1
  -o BatchMode=yes
  -o StrictHostKeyChecking=accept-new
  -o ServerAliveInterval=2
  -o ServerAliveCountMax=1
)

ssh_command_opts=(
  -p "$PORT"
  -o ConnectTimeout="$CONNECT_TIMEOUT_SECONDS"
  -o ConnectionAttempts=1
  -o BatchMode=yes
  -o StrictHostKeyChecking=accept-new
  -o ServerAliveInterval=2
  -o ServerAliveCountMax=1
  -o PasswordAuthentication=no
  -o PreferredAuthentications=publickey
)

run_timeout() {
  local seconds="$1"
  shift
  if command -v timeout >/dev/null 2>&1; then
    timeout "$seconds" "$@"
    return $?
  fi
  "$@"
}

classify_log() {
  local path="$1"
  if grep -q 'Server accepts key' "$path" && grep -Eiq 'timed out|not responding|Timeout' "$path"; then
    echo "server_accepts_key_then_timeout"
  elif grep -q 'Server accepts key' "$path"; then
    echo "server_accepts_key"
  elif grep -q 'Offering public key' "$path" && grep -Eiq 'timed out|not responding|Timeout' "$path"; then
    echo "publickey_offer_timeout"
  elif grep -q 'Permission denied' "$path"; then
    echo "permission_denied"
  elif grep -Eiq 'timed out|not responding|Timeout' "$path"; then
    echo "preauth_timeout"
  elif grep -q 'Connection established' "$path"; then
    echo "connected_unclassified"
  else
    echo "connection_failed"
  fi
}

probe_node_exporter() {
  local metrics cpu_count load1 load1_per_cpu
  metrics="$(curl -fsS --max-time "$NODE_EXPORTER_TIMEOUT_SECONDS" "http://${HOST}:${NODE_EXPORTER_PORT}/metrics" 2>/dev/null || true)"
  if [[ -z "$metrics" ]]; then
    echo "NODE_EXPORTER=unreachable"
    return
  fi
  echo "NODE_EXPORTER=ok"
  awk '
    $1 == "node_boot_time_seconds" {print "NODE_BOOT_TIME_SECONDS="$2}
    $1 == "node_time_seconds" {node_time=$2; print "NODE_TIME_SECONDS="$2}
    $1 == "node_load1" {print "NODE_LOAD1="$2}
    $1 == "node_load5" {print "NODE_LOAD5="$2}
    $1 == "node_load15" {print "NODE_LOAD15="$2}
    $1 == "node_procs_blocked" {print "NODE_PROCS_BLOCKED="$2}
    $1 == "node_memory_MemAvailable_bytes" {print "NODE_MEM_AVAILABLE_BYTES="$2}
    $1 == "node_memory_MemTotal_bytes" {print "NODE_MEM_TOTAL_BYTES="$2}
    /^node_textfile_mtime_seconds/ && /docker_stats\.prom/ {docker_stats_mtime=$NF}
    END {
      if (docker_stats_mtime != "") {
        print "DOCKER_STATS_TEXTFILE_MTIME_SECONDS=" docker_stats_mtime
        if (node_time != "") {
          age = node_time - docker_stats_mtime
          printf "DOCKER_STATS_TEXTFILE_AGE_SECONDS=%.0f\n", age
          print "DOCKER_STATS_TEXTFILE_FRESHNESS=" (age <= 300 ? "fresh" : "stale")
        }
      } else {
        print "DOCKER_STATS_TEXTFILE_FRESHNESS=missing"
      }
    }
  ' <<<"$metrics"
  cpu_count="$(awk -F'cpu="' '/^node_cpu_seconds_total/ {split($2, a, "\""); seen[a[1]]=1} END {for (cpu in seen) n++; print n+0}' <<<"$metrics")"
  load1="$(awk '$1 == "node_load1" {print $2; exit}' <<<"$metrics")"
  echo "NODE_CPU_COUNT=${cpu_count:-0}"
  if [[ -n "${load1:-}" && "${cpu_count:-0}" -gt 0 ]]; then
    load1_per_cpu="$(awk -v load="$load1" -v cpus="$cpu_count" 'BEGIN {printf "%.2f", load / cpus}')"
    echo "NODE_LOAD1_PER_CPU=$load1_per_cpu"
    awk -v ratio="$load1_per_cpu" 'BEGIN {print "NODE_LOAD_CLASSIFIER=" (ratio > 1.5 ? "high_load" : "load_not_high")}'
  fi
  awk '
    /^systemd_unit_info/ && /unit="(actions\.runner|awoooi-cd-lane)/ {
      line=$0
      unit=line
      active=line
      substate=line
      sub(/^.*unit="/, "", unit)
      sub(/".*$/, "", unit)
      sub(/^.*active_state="/, "", active)
      sub(/".*$/, "", active)
      sub(/^.*sub_state="/, "", substate)
      sub(/".*$/, "", substate)
      classifier=active
      if (active == "scrape_error" && substate ~ /(timed out|timeout)/) {
        classifier="systemctl_show_timeout"
      } else if (active == "scrape_skipped" && substate ~ /systemctl_timeout_budget_exhausted/) {
        classifier="systemctl_timeout_budget_exhausted"
      }
      printf "SYSTEMD_UNIT unit=%s active_state=%s classifier=%s\n", unit, active, classifier
    }
  ' <<<"$metrics"
}

probe_tcp_banner() {
  if command -v nc >/dev/null 2>&1; then
    if run_timeout "$CONNECT_TIMEOUT_SECONDS" nc -v "$HOST" "$PORT" </dev/null >/tmp/awoooi-110-nc.out 2>&1; then
      echo "SSH_PORT=tcp_open"
      sed -n 's/^SSH-/SSH_BANNER=SSH-/p' /tmp/awoooi-110-nc.out | head -1
      return
    fi
  fi
  echo "SSH_PORT=tcp_unconfirmed"
}

probe_user() {
  local user="$1"
  local mode="$2"
  local log="$tmp_dir/${user}-${mode}.log"
  local rc classification

  if [[ "$mode" == "publickey" ]]; then
    run_timeout "$SSH_ATTEMPT_TIMEOUT_SECONDS" \
      ssh "${ssh_base_opts[@]}" \
      -o PasswordAuthentication=no \
      -o PreferredAuthentications=publickey \
      "${user}@${HOST}" 'true' >"$log" 2>&1
    rc=$?
  else
    run_timeout "$SSH_ATTEMPT_TIMEOUT_SECONDS" \
      ssh "${ssh_base_opts[@]}" \
      -o PubkeyAuthentication=no \
      -o PreferredAuthentications=password \
      -o NumberOfPasswordPrompts=0 \
      "${user}@${HOST}" 'true' >"$log" 2>&1
    rc=$?
  fi

  classification="$(classify_log "$log")"
  printf 'SSH_AUTH user=%s mode=%s rc=%s classification=%s\n' "$user" "$mode" "$rc" "$classification"
}

probe_command_path_user() {
  local user="$1"
  local stdout_path="$tmp_dir/${user}-command-path.out"
  local stderr_path="$tmp_dir/${user}-command-path.err"
  local rc classification marker_seen remote_user remote_user_match

  run_timeout "$SSH_COMMAND_PATH_TIMEOUT_SECONDS" \
    ssh "${ssh_command_opts[@]}" \
    "${user}@${HOST}" \
    'printf "AWOOOI_110_COMMAND_PATH_READY=1\n"; printf "remote_user=%s\n" "$(id -un)"' \
    >"$stdout_path" 2>"$stderr_path"
  rc=$?

  marker_seen=false
  remote_user_match=false
  remote_user="$(awk -F= '$1 == "remote_user" {print $2; exit}' "$stdout_path" 2>/dev/null || true)"
  if grep -qx 'AWOOOI_110_COMMAND_PATH_READY=1' "$stdout_path" 2>/dev/null; then
    marker_seen=true
  fi
  if [[ "$remote_user" == "$user" ]]; then
    remote_user_match=true
  fi

  if [[ "$marker_seen" == "true" && "$remote_user_match" == "true" ]]; then
    classification="command_path_ready"
  elif grep -Eiq 'timed out|not responding|Timeout' "$stderr_path"; then
    classification="command_path_timeout"
  elif grep -q 'Permission denied' "$stderr_path"; then
    classification="command_path_permission_denied"
  else
    classification="command_path_unavailable"
  fi

  printf 'SSH_COMMAND_PATH user=%s rc=%s classification=%s marker_seen=%s remote_user_match=%s\n' \
    "$user" "$rc" "$classification" "$marker_seen" "$remote_user_match"
}

echo "AWOOOI_110_SSH_PUBLICKEY_AUTH_DIAGNOSIS"
echo "TARGET=${HOST}:${PORT}"
probe_node_exporter
probe_tcp_banner

for user in "${USERS[@]}"; do
  probe_user "$user" "publickey"
done

probe_command_path_user "$COMMAND_PATH_USER"

for user in "${USERS[@]}"; do
  probe_user "$user" "password_disabled"
done

echo "INTERPRETATION=command_path_ready_overrides_stale_verbose_true_timeout;server_accepts_key_then_timeout_without_command_path_means_check_110_session_pam_account_or_shell_path;publickey_offer_timeout_means_check_110_authorized_keys_permissions_pam_or_account_lookup_path"
