#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

readonly CONFIG_DIR="/etc/argus-agent"
readonly CONFIG_FILE="${CONFIG_DIR}/config.yml"
readonly ENV_FILE="${CONFIG_DIR}/argus-agent.env"
readonly SPOOL_DIR="/var/lib/argus-agent/spool"
readonly BIN_FILE="/usr/local/bin/argus-agent"
readonly LEGACY_CTL_FILE="/usr/local/bin/argus-agentctl"
readonly SERVICE_FILE="/etc/systemd/system/argus-agent.service"
readonly INSTALLER_VERSION="2026-11-03-ensure-tar-v10"

ARGUS_INSTALL_BASE_URL_EXPLICIT="${ARGUS_INSTALL_BASE_URL+x}"
ARGUS_INSTALL_BASE_URL="${ARGUS_INSTALL_BASE_URL:-https://argus-homolog.eulabs.net.br}"
ARGUS_SERVER_URL="${ARGUS_SERVER_URL:-http://192.168.9.80:8000}"
ARGUS_AGENT_TOKEN="${ARGUS_AGENT_TOKEN:-}"
ARGUS_PROJECT_EXPLICIT="${ARGUS_PROJECT+x}"
ARGUS_PROJECT="${ARGUS_PROJECT:-Default}"
ARGUS_AGENT_SITE_EXPLICIT="${ARGUS_AGENT_SITE+x}"
ARGUS_AGENT_SITE="${ARGUS_AGENT_SITE:-${ARGUS_SITE:-Local}}"
ARGUS_AGENT_ENVIRONMENT="${ARGUS_AGENT_ENVIRONMENT:-production}"
ARGUS_AGENT_GROUP="${ARGUS_AGENT_GROUP:-${ARGUS_GROUP:-linux}}"
ARGUS_AGENT_INTERVAL_SECONDS="${ARGUS_AGENT_INTERVAL_SECONDS:-30}"
ARGUS_AGENT_LOG_LEVEL="${ARGUS_AGENT_LOG_LEVEL:-info}"
ARGUS_FORCE_CONFIG="${ARGUS_FORCE_CONFIG:-false}"
ARGUS_PURGE_SPOOL="${ARGUS_PURGE_SPOOL:-false}"
# Interactive mode (--interactive) fills the options below; they can also be
# passed by flag/env for unattended installs. Defaults keep the historical
# behaviour: everything "auto", no remediation block (agent defaults apply).
ARGUS_INTERACTIVE="${ARGUS_INTERACTIVE:-false}"
ARGUS_PLATFORM="${ARGUS_PLATFORM:-auto}"
ARGUS_CLUSTER_NAME="${ARGUS_CLUSTER_NAME:-}"
ARGUS_K8S_ROLE="${ARGUS_K8S_ROLE:-other}"
ARGUS_SERVICES="${ARGUS_SERVICES:-auto}"
ARGUS_AUTO_CLEANUP="${ARGUS_AUTO_CLEANUP:-}"
ARGUS_MYSQL_DSN="${ARGUS_MYSQL_DSN:-}"
ARGUS_POSTGRES_DSN="${ARGUS_POSTGRES_DSN:-}"
# Argus APM (--with-apm): installs the argus-apm component (systemd unit, not
# started here: the agent starts it when the site policy asks for it).
ARGUS_WITH_APM="${ARGUS_WITH_APM:-false}"
ARGUS_APM_PROPAGATION="${ARGUS_APM_PROPAGATION:-false}"
# --allow-apm-activation: lets this agent write the Argus APM systemd drop-in
# of approved level-2 plans (AGENTS.md 10.1). Off by default.
ARGUS_APM_ACTIVATION="${ARGUS_APM_ACTIVATION:-false}"
AGENT_ACTIVATION_DROPIN="/etc/systemd/system/argus-agent.service.d/20-apm-activation.conf"
APM_BIN_FILE="/usr/local/bin/argus-apm"
APM_SERVICE_FILE="/etc/systemd/system/argus-apm.service"
APM_DROPIN_DIR="/etc/systemd/system/argus-apm.service.d"
APM_CONFIG_DIR="/etc/argus-apm"
APM_STATE_DIR="/var/lib/argus-apm"

# Site profile overlay (Admin -> perfil do site) fetched with the token; empty
# when the token has no profile or the server could not be reached.
PROFILE_OVERLAY_FILE=""

readonly -a KNOWN_PLATFORMS=(auto linux docker swarm k8s proxmox)
readonly -a KNOWN_SERVICES=(nginx apache haproxy traefik postfix mysql mariadb postgresql redis)

cleanup_temp_file() {
  if [ -n "${1:-}" ]; then
    rm -f -- "$1"
  fi
}

trim_quotes() {
  local value="$1"
  value="${value%\"}"
  value="${value#\"}"
  value="${value%\'}"
  value="${value#\'}"
  printf '%s' "$value"
}

read_agent_config_value() {
  local key="$1"
  local current_section=""
  local line trimmed section_name value

  [ -f "$CONFIG_FILE" ] || return 1

  while IFS= read -r line; do
    trimmed="${line#"${line%%[![:space:]]*}"}"
    case "$trimmed" in
      "" | \#*)
        continue
        ;;
    esac
    if [ "${line# }" = "$line" ] && [ "${trimmed%:}" != "$trimmed" ]; then
      section_name="${trimmed%:}"
      current_section="$section_name"
      continue
    fi
    if [ "$current_section" = "agent" ] && [ "${trimmed#${key}:}" != "$trimmed" ]; then
      value="${trimmed#${key}:}"
      value="${value#"${value%%[![:space:]]*}"}"
      trim_quotes "$value"
      return 0
    fi
  done <"$CONFIG_FILE"

  return 1
}

preserve_existing_agent_identity() {
  local existing_site

  if [ -n "$ARGUS_AGENT_SITE_EXPLICIT" ]; then
    return
  fi
  if existing_site="$(read_agent_config_value "site")" && [ -n "$existing_site" ]; then
    ARGUS_AGENT_SITE="$existing_site"
    log "preserving existing agent.site=${ARGUS_AGENT_SITE} to keep stable agent_id"
  fi
}

log() {
  printf '[argus-agent] %s\n' "$*"
}

die() {
  printf '[argus-agent] ERROR: %s\n' "$*" >&2
  exit 1
}

usage() {
  cat <<'EOF'
Usage:
  install-argus-agent.sh [install] [--project NAME] [--site NAME] [--environment NAME] [--group NAME] [--server-url URL] [--install-base-url URL] [--token TOKEN] [--force-config]
                         [--interactive] [--platform auto|linux|docker|swarm|k8s|proxmox] [--cluster-name NAME]
                         [--k8s-role server|agent|other] [--services auto|none|LIST] [--auto-cleanup true|false]
                         [--with-apm] [--apm-propagation]
  install-argus-agent.sh uninstall
  install-argus-agent.sh remove

Interactive install (asks every question, shows the plan, then confirms):
  curl -fsSL https://argus-homolog.eulabs.net.br/install-argus-agent.sh -o /tmp/install-argus-agent.sh
  sudo bash /tmp/install-argus-agent.sh --interactive
Interactive mode also starts automatically when no token is given and a terminal is available.

Options:
  --platform        What this host is. auto = let the agent discover (default).
  --cluster-name    Friendly cluster name (swarm/k8s). Use the same name on every node.
  --k8s-role        RKE2 role for --platform k8s: server, agent or other (k3s/kubeadm/managed).
  --services        auto (discovery decides), none, or a comma list of:
                    nginx,apache,haproxy,traefik,postfix,mysql,mariadb,postgresql,redis
  --auto-cleanup    true = the agent may prune unused Docker images/build cache/stopped
                    containers and truncate big container logs when / reaches 90%.
                    Volumes are never removed. false = alert only.
  --with-apm        Also install Argus APM (argus-apm.service). Nothing is collected
                    until the site policy enables it; requires a site token.
  --apm-propagation Grant argus-apm the extra capabilities for eBPF context
                    propagation (CAP_NET_ADMIN, CAP_SYS_ADMIN). Off by default.
  --allow-apm-activation
                    Let the agent write the Argus APM drop-in
                    (/etc/systemd/system/<unit>.d/90-argus-apm.conf) of approved
                    level-2 plans. Never restarts the service. Off by default.

Environment:
  ARGUS_AGENT_TOKEN             Required for install. Token sent to Argus Server.
  ARGUS_PROJECT                 Project name. Default: Default
  ARGUS_AGENT_SITE              Identity namespace for this host. Default: default
                                 (changing it after install changes the agent_id).
  ARGUS_AGENT_GROUP             Agent group. Default: linux
  ARGUS_SERVER_URL              Default: http://192.168.9.80:8000
  ARGUS_INSTALL_BASE_URL        Default: https://argus-homolog.eulabs.net.br, or --server-url when passed by CLI.
  ARGUS_FORCE_CONFIG=true       Overwrite /etc/argus-agent/config.yml if it exists.
  ARGUS_PURGE_SPOOL=true        Remove /var/lib/argus-agent on uninstall.
  ARGUS_INTERACTIVE=true        Same as --interactive.
  ARGUS_PLATFORM, ARGUS_CLUSTER_NAME, ARGUS_K8S_ROLE, ARGUS_SERVICES, ARGUS_AUTO_CLEANUP
                                Same as the flags above.
  ARGUS_WITH_APM=true, ARGUS_APM_PROPAGATION=true, ARGUS_APM_ACTIVATION=true
                                Same as --with-apm / --apm-propagation / --allow-apm-activation.
  ARGUS_MYSQL_DSN, ARGUS_POSTGRES_DSN
                                Optional DSNs written to the env file (0600).
EOF
}

parse_args() {
  COMMAND="install"
  while [ "$#" -gt 0 ]; do
    case "$1" in
      install | uninstall | remove)
        COMMAND="$1"
        shift
        ;;
      --project)
        [ -n "${2:-}" ] || die "--project requires a value"
        ARGUS_PROJECT="${2:-}"
        ARGUS_PROJECT_EXPLICIT=1
        shift 2
        ;;
      --site)
        [ -n "${2:-}" ] || die "--site requires a value"
        ARGUS_AGENT_SITE="${2:-}"
        ARGUS_AGENT_SITE_EXPLICIT=1
        shift 2
        ;;
      --environment)
        [ -n "${2:-}" ] || die "--environment requires a value"
        ARGUS_AGENT_ENVIRONMENT="${2:-}"
        shift 2
        ;;
      --group)
        [ -n "${2:-}" ] || die "--group requires a value"
        ARGUS_AGENT_GROUP="${2:-}"
        shift 2
        ;;
      --server-url)
        [ -n "${2:-}" ] || die "--server-url requires a value"
        ARGUS_SERVER_URL="${2:-}"
        if [ -z "$ARGUS_INSTALL_BASE_URL_EXPLICIT" ]; then
          ARGUS_INSTALL_BASE_URL="$ARGUS_SERVER_URL"
        fi
        shift 2
        ;;
      --install-base-url)
        [ -n "${2:-}" ] || die "--install-base-url requires a value"
        ARGUS_INSTALL_BASE_URL="${2:-}"
        ARGUS_INSTALL_BASE_URL_EXPLICIT=1
        shift 2
        ;;
      --token)
        [ -n "${2:-}" ] || die "--token requires a value"
        ARGUS_AGENT_TOKEN="${2:-}"
        shift 2
        ;;
      --force-config)
        ARGUS_FORCE_CONFIG=true
        shift
        ;;
      --purge-spool)
        ARGUS_PURGE_SPOOL=true
        shift
        ;;
      -i | --interactive)
        ARGUS_INTERACTIVE=true
        shift
        ;;
      --with-apm)
        ARGUS_WITH_APM=true
        shift
        ;;
      --apm-propagation)
        ARGUS_WITH_APM=true
        ARGUS_APM_PROPAGATION=true
        shift
        ;;
      --allow-apm-activation)
        ARGUS_WITH_APM=true
        ARGUS_APM_ACTIVATION=true
        shift
        ;;
      --platform | --cluster-name | --k8s-role | --services | --auto-cleanup)
        [ -n "${2:-}" ] || die "$1 requires a value"
        case "$1" in
          --platform) ARGUS_PLATFORM="$2" ;;
          --cluster-name) ARGUS_CLUSTER_NAME="$2" ;;
          --k8s-role) ARGUS_K8S_ROLE="$2" ;;
          --services) ARGUS_SERVICES="$2" ;;
          --auto-cleanup) ARGUS_AUTO_CLEANUP="$2" ;;
        esac
        shift 2
        ;;
      -h | --help | help)
        COMMAND="help"
        shift
        ;;
      *)
        usage
        die "unknown argument: $1"
        ;;
    esac
  done
}

require_root() {
  if [ "$(id -u)" -ne 0 ]; then
    die "run as root or with sudo"
  fi
}

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

# Minimal images (Oracle Linux, UBI) ship without tar; the APM bundle needs it.
ensure_tar() {
  command -v tar >/dev/null 2>&1 && return 0
  log "tar not found; installing it"
  if command -v dnf >/dev/null 2>&1; then
    dnf -y -q install tar >/dev/null 2>&1 || true
  elif command -v yum >/dev/null 2>&1; then
    yum -y -q install tar >/dev/null 2>&1 || true
  elif command -v microdnf >/dev/null 2>&1; then
    microdnf -y install tar >/dev/null 2>&1 || true
  elif command -v apt-get >/dev/null 2>&1; then
    DEBIAN_FRONTEND=noninteractive apt-get install -y -q tar >/dev/null 2>&1 || true
  fi
  require_cmd tar
}

arch_suffix() {
  case "$(uname -m)" in
    x86_64 | amd64)
      printf 'amd64'
      ;;
    aarch64 | arm64)
      printf 'arm64'
      ;;
    *)
      die "unsupported architecture: $(uname -m)"
      ;;
  esac
}

download_file() {
  local url="$1"
  local output="$2"

  if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$url" -o "$output"
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$output" "$url"
  else
    die "curl or wget is required to download $url"
  fi
}

sha256_file() {
  local file="$1"

  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$file" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$file" | awk '{print $1}'
  else
    die "sha256sum or shasum is required to validate downloads"
  fi
}

validate_checksum() {
  local binary_path="$1"
  local binary_name="$2"
  local checksums_file="$3"
  local expected actual

  [ -s "$checksums_file" ] || die "checksum file is empty"
  expected="$(awk -v name="$binary_name" '$2 == name {print $1}' "$checksums_file" | head -n 1)"
  [ -n "$expected" ] || die "checksum for $binary_name not found"
  actual="$(sha256_file "$binary_path")"
  if [ "$actual" != "$expected" ]; then
    die "checksum mismatch for $binary_name"
  fi
}

write_service() {
  cat >"$SERVICE_FILE" <<'EOF'
[Unit]
Description=Argus Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=-/etc/argus-agent/argus-agent.env
ExecStart=/usr/local/bin/argus-agent --config /etc/argus-agent/config.yml
Restart=always
RestartSec=10
User=root
# Hardening: the agent only reads the host (and writes its own state under
# /var/lib/argus-agent). Remediation reaches docker/systemd through their
# sockets/D-Bus (and the docker CLI, which uses $HOME/.docker -- hence no
# ProtectHome), which this keeps working. To relax a setting on one host,
# use a drop-in (systemctl edit argus-agent) instead of editing this file.
NoNewPrivileges=true
ProtectSystem=full
# Argus APM: the agent renders /etc/argus-apm/config.yaml (only that dir).
ReadWritePaths=-/etc/argus-apm
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectClock=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
SystemCallArchitectures=native
# Reads of other users' files and /proc/<pid>/fd, ping's raw socket.
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_SYS_PTRACE CAP_NET_RAW
# The agent must never compete with the workloads it monitors.
CPUWeight=50
MemoryHigh=256M
MemoryMax=512M
TasksMax=256

[Install]
WantedBy=multi-user.target
EOF
  chmod 0644 "$SERVICE_FILE"
}

write_env() {
  local temp_env kept_lines=""
  local -a drop=(-e '^ARGUS_AGENT_TOKEN=' -e '^ARGUS_SERVER_URL=')

  umask 077
  # Keep lines an operator added by hand (e.g. a DSN) unless this run sets them.
  [ -z "$ARGUS_MYSQL_DSN" ] || drop+=(-e '^ARGUS_MYSQL_DSN=')
  [ -z "$ARGUS_POSTGRES_DSN" ] || drop+=(-e '^ARGUS_POSTGRES_DSN=')
  if [ -f "$ENV_FILE" ]; then
    kept_lines="$(grep -v "${drop[@]}" "$ENV_FILE" || true)"
  fi
  temp_env="$(mktemp "${ENV_FILE}.XXXXXX")"
  {
    printf 'ARGUS_AGENT_TOKEN=%s\n' "$ARGUS_AGENT_TOKEN"
    printf 'ARGUS_SERVER_URL=%s\n' "$ARGUS_SERVER_URL"
    [ -z "$ARGUS_MYSQL_DSN" ] || printf 'ARGUS_MYSQL_DSN=%s\n' "$ARGUS_MYSQL_DSN"
    [ -z "$ARGUS_POSTGRES_DSN" ] || printf 'ARGUS_POSTGRES_DSN=%s\n' "$ARGUS_POSTGRES_DSN"
    [ -z "$kept_lines" ] || printf '%s\n' "$kept_lines"
  } >"$temp_env"
  chmod 0600 "$temp_env"
  mv "$temp_env" "$ENV_FILE"
}

# Reinstalling with an explicit --project/ARGUS_PROJECT must move the host:
# the server only migrates it when the project it registers with changes, and
# a kept config.yml would otherwise still carry the old one.
update_existing_config_project() {
  local temp_config

  [ -n "$ARGUS_PROJECT_EXPLICIT" ] || return 0
  if [ "$(read_agent_config_value "project" || true)" = "$ARGUS_PROJECT" ]; then
    return 0
  fi
  temp_config="$(mktemp "${CONFIG_FILE}.XXXXXX")"
  # Replaces agent.project, or inserts it at the end of the agent: section
  # when absent (configs written by older installers have no project key).
  awk -v project="$ARGUS_PROJECT" '
    /^[^[:space:]#][^:]*:/ {
      if (section == "agent" && !done) {
        print (indent == "" ? "  " : indent) "project: " project
        done = 1
      }
      section = $0; sub(/:.*/, "", section)
      if (section == "agent") seen = 1
      print
      next
    }
    section == "agent" && indent == "" && /^[[:space:]]+[^[:space:]#]/ {
      match($0, /^[[:space:]]+/)
      indent = substr($0, 1, RLENGTH)
    }
    section == "agent" && !done && /^[[:space:]]+project:/ {
      print (indent == "" ? "  " : indent) "project: " project
      done = 1
      next
    }
    { print }
    END {
      if (section == "agent" && !done) { print (indent == "" ? "  " : indent) "project: " project; done = 1 }
      if (!seen || !done) exit 3
    }
  ' "$CONFIG_FILE" >"$temp_config" || {
    rm -f "$temp_config"
    die "could not update agent.project in ${CONFIG_FILE}; rerun with --force-config"
  }
  cp -p "$CONFIG_FILE" "${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)"
  chmod --reference="$CONFIG_FILE" "$temp_config" 2>/dev/null || chmod 0644 "$temp_config"
  mv "$temp_config" "$CONFIG_FILE"
  log "updated agent.project=${ARGUS_PROJECT} in existing ${CONFIG_FILE}"
}

# Downloads the site profile overlay bound to the token. 200 -> overlay file,
# 204 -> token without profile, 401/403 -> abort (invalid/revoked token).
# Network errors, 404 (server without profile support) and 5xx only warn:
# the install goes on with the historical behaviour.
fetch_profile_overlay() {
  local url="${ARGUS_SERVER_URL%/}/api/v1/agents/enroll/profile"
  local output status

  if ! command -v curl >/dev/null 2>&1; then
    log "WARNING: curl not found; site profile not applied"
    return 0
  fi
  output="$(mktemp)"
  status="$(curl -sS -o "$output" -w '%{http_code}' --connect-timeout 5 --max-time 15 \
    -H "Authorization: Bearer ${ARGUS_AGENT_TOKEN}" "$url" 2>/dev/null)" || status="${status:-000}"
  case "$status" in
    200)
      PROFILE_OVERLAY_FILE="$output"
      log "site profile v$(sed -n 's/.*"profile_version": *\([0-9][0-9]*\).*/\1/p' "$output") received"
      return 0
      ;;
    204)
      log "token has no site profile; keeping the default config behaviour"
      ;;
    401 | 403)
      rm -f "$output"
      die "agent token rejected by ${ARGUS_SERVER_URL} (invalid, expired or revoked)"
      ;;
    *)
      log "WARNING: could not fetch the site profile (HTTP ${status}); profile not applied"
      ;;
  esac
  rm -f "$output"
}

# Runs `argus-agent config merge` with the overlay: adds missing keys, forces
# `true` on the profile's marked items, appends its automatic actions, never
# writes false, keeps comments and writes config.yml.bak.<date>. Returns
# non-zero (config untouched) when the binary has no merge subcommand or the
# merge refuses the local config.
apply_profile_overlay() {
  local output rc=0 line

  [ -n "$PROFILE_OVERLAY_FILE" ] || return 1
  # Checked by content: an old binary would treat "config merge" as plain
  # args and start the agent in the foreground.
  if ! grep -aq 'config merge: --overlay is required' "$BIN_FILE"; then
    log "WARNING: ${BIN_FILE} has no 'config merge'; site profile not applied"
    return 1
  fi
  output="$("$BIN_FILE" config merge --config "$CONFIG_FILE" --overlay "$PROFILE_OVERLAY_FILE" 2>&1)" || rc=$?
  while IFS= read -r line; do
    [ -z "$line" ] || log "profile: ${line}"
  done <<<"$output"
  if [ "$rc" -ne 0 ]; then
    log "WARNING: config merge failed (exit ${rc}); site profile not applied to ${CONFIG_FILE}"
    return 1
  fi
  return 0
}

# Comma-joins its arguments (the script-wide IFS is newline/tab).
join_by() {
  local IFS=','
  printf '%s' "$*"
}

in_list() {
  local needle="$1" item
  shift
  for item in "$@"; do
    [ "$item" = "$needle" ] && return 0
  done
  return 1
}

services_list() {
  local -a items=()
  IFS=',' read -r -a items <<<"${ARGUS_SERVICES// /}"
  printf '%s\n' "${items[@]}"
}

validate_install_options() {
  local service

  in_list "$ARGUS_PLATFORM" "${KNOWN_PLATFORMS[@]}" ||
    die "--platform must be one of: $(join_by "${KNOWN_PLATFORMS[@]}")"
  in_list "$ARGUS_K8S_ROLE" server agent other || die "--k8s-role must be server, agent or other"
  case "$ARGUS_AUTO_CLEANUP" in
    "" | true | false) ;;
    *) die "--auto-cleanup must be true or false" ;;
  esac
  # Written verbatim into YAML: keep it to a safe charset.
  if [ -n "$ARGUS_CLUSTER_NAME" ] && ! [[ "$ARGUS_CLUSTER_NAME" =~ ^[A-Za-z0-9._-]{1,63}$ ]]; then
    die "--cluster-name accepts only letters, digits, '.', '_' and '-' (max 63)"
  fi
  case "$ARGUS_SERVICES" in
    auto | none) ;;
    *)
      while IFS= read -r service; do
        [ -n "$service" ] || continue
        in_list "$service" "${KNOWN_SERVICES[@]}" ||
          die "unknown service '${service}' in --services (known: $(join_by "${KNOWN_SERVICES[@]}"))"
      done < <(services_list)
      ;;
  esac
}

# enabled: value for an optional service collector.
service_mode() {
  local name="$1"
  case "$ARGUS_SERVICES" in
    auto)
      # Historical defaults: discovery decides, except collectors that need
      # explicit opt-in.
      case "$name" in
        traefik | mariadb) printf 'false' ;;
        *) printf 'auto' ;;
      esac
      ;;
    none) printf 'false' ;;
    *)
      if services_list | grep -qx "$name"; then printf 'true'; else printf 'false'; fi
      ;;
  esac
}

# Sets CFG_* (enabled: values per platform collector) from ARGUS_PLATFORM.
resolve_platform_modes() {
  CFG_DOCKER=auto CFG_SWARM=auto CFG_K8S=auto CFG_VIRT=auto CFG_PROXMOX=auto CFG_LIBVIRT=auto
  CFG_RKE2_SERVER=false CFG_RKE2_AGENT=false
  case "$ARGUS_PLATFORM" in
    linux)
      CFG_DOCKER=false CFG_SWARM=false CFG_K8S=false CFG_VIRT=false CFG_PROXMOX=false CFG_LIBVIRT=false
      ;;
    docker)
      CFG_DOCKER=true CFG_SWARM=false CFG_K8S=false CFG_VIRT=false CFG_PROXMOX=false CFG_LIBVIRT=false
      ;;
    swarm)
      CFG_DOCKER=true CFG_SWARM=true CFG_K8S=false CFG_VIRT=false CFG_PROXMOX=false CFG_LIBVIRT=false
      ;;
    k8s)
      CFG_SWARM=false CFG_K8S=true CFG_VIRT=false CFG_PROXMOX=false CFG_LIBVIRT=false
      [ "$ARGUS_K8S_ROLE" != server ] || CFG_RKE2_SERVER=true
      [ "$ARGUS_K8S_ROLE" != agent ] || CFG_RKE2_AGENT=true
      ;;
    proxmox)
      # Proxmox runs QEMU/KVM without libvirt.
      CFG_SWARM=false CFG_K8S=false CFG_VIRT=true CFG_PROXMOX=true CFG_LIBVIRT=false
      ;;
  esac
}

# The agent only executes a remediation the server asks for when the action is
# in remediation.allowed_actions *and* enabled here, so this block is the
# host-side switch for automatic cleanup. Empty ARGUS_AUTO_CLEANUP keeps the
# historical behaviour (no block: agent defaults, nothing allowed).
render_remediation_block() {
  case "$ARGUS_AUTO_CLEANUP" in
    true)
      cat <<'EOF'
# Automatic cleanup (chosen at install). The agent only acts when the server
# asks (disk / >= 90%), revalidates first, and never removes volumes or
# containers/images with a protected label.
remediation:
  enabled: true
  allow_arbitrary_commands: false
  allowed_actions:
    - docker_disk_cleanup_dry_run
    - docker_disk_cleanup_controlled
    - journald_vacuum_dry_run
    - journald_vacuum
    # fail2ban no SSH (so plano aprovado): instala o pacote e grava jail.d/argus-sshd.local
    - fail2ban_sshd_protect
    # CVE de pacote (so plano aprovado no detalhe da vulnerabilidade): atualiza os pacotes, nunca kernel
    - package_upgrade
  docker_disk_cleanup:
    enabled: true
    mode: safe_auto
    partition: /
    threshold_percent: 90
    critical_threshold_percent: 95
    cooldown_seconds: 86400
    prune_images: true
    prune_build_cache: true
    prune_containers: true
    prune_volumes: false
    allow_volume_prune: false
    truncate_container_logs: true
    container_log_max_size_mb: 100
    truncate_only_if_disk_above_threshold: true
    protected_labels:
      - argus.protect=true
      - backup=true
      - database=true
  journald_vacuum:
    enabled: true
    mode: approval_required
    partition: /
    threshold_percent: 85
    vacuum_time_days: 7
    vacuum_size_mb: 500

EOF
      ;;
    false)
      cat <<'EOF'
# Automatic cleanup disabled at install: alerts only, the agent executes no
# remediation on this host.
remediation:
  enabled: true
  allow_arbitrary_commands: false
  allowed_actions: []

EOF
      ;;
  esac
}

write_config() {
  local target_config="$CONFIG_FILE"
  local remediation_block

  resolve_platform_modes
  remediation_block="$(render_remediation_block)"

  if [ -f "$CONFIG_FILE" ] && [ "$ARGUS_FORCE_CONFIG" != "true" ]; then
    update_existing_config_project
    target_config="${CONFIG_FILE}.new"
    log "config already exists, keeping ${CONFIG_FILE} and writing ${target_config}"
    if [ "$ARGUS_PLATFORM" != auto ] || [ -n "$ARGUS_CLUSTER_NAME" ] ||
      [ "$ARGUS_SERVICES" != auto ] || [ -n "$ARGUS_AUTO_CLEANUP" ]; then
      log "WARNING: platform/cluster/services/cleanup options are only in ${target_config};" \
        "rerun with --force-config to apply them"
    fi
  elif [ -f "$CONFIG_FILE" ]; then
    cp -p "$CONFIG_FILE" "${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)"
  fi

  cat >"$target_config" <<EOF
# Argus Agent default configuration.
#
# Secure by default: only host, CPU, memory, filesystem and systemd health
# collectors are enabled. Service-specific collectors stay disabled until an
# operator enables only what belongs to this host.

agent:
  name: ""
  project: ${ARGUS_PROJECT}
  site: ${ARGUS_AGENT_SITE}
  environment: ${ARGUS_AGENT_ENVIRONMENT}
  agent_group: ${ARGUS_AGENT_GROUP}
  interval_seconds: ${ARGUS_AGENT_INTERVAL_SECONDS}
  spool_dir: /var/lib/argus-agent/spool
  log_level: ${ARGUS_AGENT_LOG_LEVEL}

server:
  url: ${ARGUS_SERVER_URL}
  token_env: ARGUS_AGENT_TOKEN
  timeout_seconds: 10

remote_policy:
  enabled: true
  refresh_interval_seconds: 60
  cache_file: /var/lib/argus-agent/policy-cache.json

discovery:
  enabled: true
  mode: safe
  scan_interval_seconds: 300
  deep_scan_interval_seconds: 1800
  local_only: true
  network_scan_enabled: false
  max_ports_to_probe: 50
  auto_enable_collectors: true
  auto_enable_safe_collectors: true
  auto_enable_requires_credentials: false
  send_discovered_services: true
  send_discovery_evidence: true
  ignore_services: []
  include_services: []

${remediation_block}
identity:
  hostname_override: ""
  ip_detection: auto
  ip_override: ""
  ip_detection_interface: ""
  prefer_ipv4: true
  ignore_interfaces:
    - lo
    - docker0
    - br-*
    - veth*
    - cni*
    - flannel*
    - cali*
  tags: []

collectors:
  # Basic OS identity, uptime and load/kernel metadata.
  host:
    enabled: true
    collect_uptime: true
    collect_load_average: true
    collect_os_release: true
    collect_kernel: true

  # CPU usage check and metric.
  cpu:
    enabled: true
    warning_percent: 80
    critical_percent: 90
    sample_seconds: 2

  # Memory usage check and metric.
  memory:
    enabled: true
    warning_percent: 80
    critical_percent: 90

  # Filesystem usage check and metric. Keep pseudo filesystems excluded.
  filesystem:
    enabled: true
    warning_percent: 85
    critical_percent: 90
    include_mountpoints:
      - /
      - /var
      - /opt
      - /home
      - /docker
      - /var/lib/docker
    exclude_filesystem_types:
      - tmpfs
      - devtmpfs
      - overlay
      - squashfs
      - proc
      - sysfs
      - cgroup2
    exclude_mountpoints:
      - /proc
      - /sys
      - /dev
      - /run
      - /snap

  # systemd services. The agent detects the distro (/etc/os-release) and
  # picks the correct default service list automatically (e.g. cron on
  # Debian/Ubuntu vs crond on Oracle Linux/RHEL-like) — you don't need to
  # list both. Use extra_services for anything specific to this host.
  systemd:
    enabled: true
    service_profile: auto
    use_os_defaults: true
    extra_services: []
    ignored_services: []
    alert_on_missing_extra_services: false
    missing_default_service_state: skipped
    inactive_is_critical: false
    failed_is_critical: true
    required_services_are_critical: true

  # Systemd degraded/failed units. reset-failed is safe here because it only
  # clears accumulated failed state and never restarts a service.
  systemd_health:
    enabled: true
    check_degraded: true
    collect_failed_units: true
    max_failed_units: 20
    auto_reset_failed: true
    reset_failed_after_notify: true
    reset_failed_delay_seconds: 30
    revalidate_after_seconds: 10
    reset_failed_cooldown_seconds: 3600
    commands_timeout_seconds: 10
    ignored_units:
      - session-*.scope
      - user@*.service

  # Top processes by CPU/memory and optional systemd unit aggregation.
  resource_top:
    enabled: true
    interval_seconds: 30
    top_n: 10
    collect_processes: true
    collect_systemd_units: true
    collect_docker_containers: true
    collect_cpu: true
    collect_memory: true
    collect_io: true
    sample_seconds: 2
    min_cpu_percent: 1
    min_memory_percent: 1
    include_cmdline: true
    cmdline_max_length: 180
    aggregate_by_systemd_unit: true
    aggregate_by_container: true
    aggregate_by_user: true
    ignore_kernel_threads: true
    ignore_processes:
      - kthreadd
      - kworker/*
      - ksoftirqd/*
      - rcu_*
    alert_cpu_process_percent: 80
    alert_memory_process_percent: 50
    send_even_without_alert: true

  # Top directories by size. Runs only when filesystem usage is high.
  disk_top:
    enabled: true
    only_when_filesystem_usage_gte: 80
    top_n: 5
    max_depth: 2
    timeout_seconds: 20
    min_interval_seconds: 900
    same_filesystem_only: true
    exclude_paths:
      - /proc
      - /sys
      - /dev
      - /run
      - /tmp/systemd-private*
      - /var/lib/kubelet/pods
      - /var/lib/docker/overlay2/*/merged

  # Optional collectors. Enable only on hosts that actually run these services.
  docker:
    enabled: ${CFG_DOCKER}
    socket: /var/run/docker.sock
    timeout_seconds: 10
    collect_engine: true
    collect_df: true
    collect_containers: true
    collect_container_stats: true
    collect_images: true
    collect_volumes: true
    image_usage_warning_gb: 10
    image_usage_critical_gb: 20
    volume_usage_warning_gb: 20
    volume_usage_critical_gb: 50
    unused_images:
      enabled: true
      older_than_hours: 168
      include_dangling: true
    unused_volumes:
      enabled: true
      protect_named_volumes: true
      require_label_for_auto_cleanup: true
      allowed_cleanup_label: argus.cleanup=true
    cleanup:
      enabled: false
      dry_run: true
      notify_before_cleanup: true
      cleanup_delay_seconds: 30
      cooldown_seconds: 86400
      images:
        enabled: false
        remove_unused: true
        remove_dangling: true
        older_than_hours: 168
        max_reclaim_gb: 10
      volumes:
        enabled: false
        remove_unused: false
        require_label: true
        allowed_label: argus.cleanup=true
        max_reclaim_gb: 10

  docker_swarm:
    enabled: ${CFG_SWARM}
    socket: /var/run/docker.sock
    timeout_seconds: 10
    manager_only_checks: true
    collect_nodes: true
    collect_services: true
    collect_tasks: true
    collect_networks: true
    collect_overlay_networks: true
    collect_manager_quorum: true
    api_ping_interval_seconds: 30
    swarm_check_interval_seconds: 60
    expected_managers_min: 3
    manager_quorum_required: true
    alert_on_node_down: true
    alert_on_node_availability_drain: true
    alert_on_node_availability_pause: true
    alert_on_service_replica_mismatch: true
    alert_on_task_failed: true
    alert_on_overlay_network_issue: true
    ignored_nodes: []
    ignored_services: []
    ignored_networks: []
    # Friendly name on the Clusters page; use the same value on every manager.
    cluster_name: "${ARGUS_CLUSTER_NAME}"
    cluster_checks_scope: leader

  # Virtualization (Proxmox VE uses QEMU/KVM directly, without libvirt).
  kvm:
    enabled: ${CFG_VIRT}

  qemu:
    enabled: ${CFG_VIRT}

  libvirt:
    enabled: ${CFG_LIBVIRT}

  proxmox_ve:
    enabled: ${CFG_PROXMOX}
    mode: local_readonly

  proxmox_backup:
    enabled: ${CFG_PROXMOX/true/auto}

  haproxy:
    enabled: $(service_mode haproxy)
    stats_url: http://127.0.0.1:8404/metrics
    stats_socket_paths:
      - /run/haproxy/admin.sock
      - /var/lib/haproxy/stats
      - /var/run/haproxy.sock
    collect_backends: true
    collect_frontends: true

  rke2_server:
    enabled: ${CFG_RKE2_SERVER}
    services:
      - rke2-server
    kubeconfig: /etc/rancher/rke2/rke2.yaml

  rke2_agent:
    enabled: ${CFG_RKE2_AGENT}
    services:
      - rke2-agent
    kubeconfig: /etc/rancher/rke2/rke2.yaml

  traefik:
    enabled: $(service_mode traefik)
    metrics_url: http://127.0.0.1:8080/metrics

  nginx:
    enabled: $(service_mode nginx)
    status_url: http://127.0.0.1/nginx_status
    endpoints:
      - http://127.0.0.1/nginx_status

  apache:
    enabled: $(service_mode apache)
    endpoints:
      - http://127.0.0.1/server-status?auto

  postfix:
    enabled: $(service_mode postfix)
    queue_command: postqueue -p

  mysql:
    enabled: $(service_mode mysql)
    require_credentials: true
    dsn_env: ARGUS_MYSQL_DSN
    service_names:
      - mysql
      - mysqld
    check_port: 3306

  mariadb:
    enabled: $(service_mode mariadb)
    service_names:
      - mariadb
      - mysql
    check_port: 3306

  postgresql:
    enabled: $(service_mode postgresql)
    require_credentials: true
    dsn_env: ARGUS_POSTGRES_DSN
    service_names:
      - postgresql
    check_port: 5432

  redis:
    enabled: $(service_mode redis)
    service_names:
      - redis
      - redis-server
    check_port: 6379

  kubernetes:
    enabled: ${CFG_K8S}
    mode: discovery_only
    # Reserved for the Kubernetes cluster view (same value on every node).
    cluster_name: "${ARGUS_CLUSTER_NAME}"
EOF
  chmod 0644 "$target_config"
}

# --- Interactive mode -------------------------------------------------------
# Prompts read/write /dev/tty so they work under "curl ... | sudo bash".

tty_available() {
  { : </dev/tty; } 2>/dev/null
}

say() {
  printf '%s\n' "$*" >/dev/tty
}

ask() {
  local prompt="$1" default="${2:-}" answer=""
  if [ -n "$default" ]; then
    printf '%s [%s]: ' "$prompt" "$default" >/dev/tty
  else
    printf '%s: ' "$prompt" >/dev/tty
  fi
  IFS= read -r answer </dev/tty || true
  printf '%s' "${answer:-$default}"
}

ask_secret() {
  local prompt="$1" answer=""
  printf '%s: ' "$prompt" >/dev/tty
  IFS= read -rs answer </dev/tty || true
  printf '\n' >/dev/tty
  printf '%s' "$answer"
}

# Returns 0 for yes. $2 is the default: s or n.
ask_yes_no() {
  local prompt="$1" default="$2" answer="" hint="[s/N]"
  [ "$default" = "s" ] && hint="[S/n]"
  while true; do
    printf '%s %s: ' "$prompt" "$hint" >/dev/tty
    IFS= read -r answer </dev/tty || true
    case "$(printf '%s' "${answer:-$default}" | tr '[:upper:]' '[:lower:]')" in
      s | sim | y | yes) return 0 ;;
      n | nao | no) return 1 ;;
    esac
    say "  Responda s ou n."
  done
}

# Prints the chosen option number (1-based). $2 is the default number.
ask_choice() {
  local prompt="$1" default="$2" answer="" option i=1
  shift 2
  say "$prompt"
  for option in "$@"; do
    say "  ${i}) ${option}"
    i=$((i + 1))
  done
  while true; do
    printf 'Opcao [%s]: ' "$default" >/dev/tty
    IFS= read -r answer </dev/tty || true
    answer="${answer:-$default}"
    if [[ "$answer" =~ ^[0-9]+$ ]] && [ "$answer" -ge 1 ] && [ "$answer" -le "$#" ]; then
      printf '%s' "$answer"
      return 0
    fi
    say "  Opcao invalida."
  done
}

run_quiet() {
  if command -v timeout >/dev/null 2>&1; then
    timeout 5 "$@" 2>/dev/null
  else
    "$@" 2>/dev/null
  fi
}

systemd_units() {
  systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' || true
}

has_unit() {
  # $1: installed unit list, $2: ERE of unit base names.
  printf '%s\n' "$1" | grep -qE "^($2)(@[^.]*)?\\.service\$"
}

detect_platform() {
  local units
  units="$(systemd_units)"
  if [ -d /etc/pve ] || command -v pveversion >/dev/null 2>&1; then
    printf 'proxmox'
  elif [ -d /etc/rancher/rke2 ] || [ -d /etc/rancher/k3s ] ||
    has_unit "$units" 'kubelet|k3s|k3s-agent|rke2-server|rke2-agent'; then
    # Before Swarm: a K8s node may keep a leftover active Swarm.
    printf 'k8s'
  elif command -v docker >/dev/null 2>&1 &&
    [ "$(run_quiet docker info --format '{{.Swarm.LocalNodeState}}' || true)" = "active" ]; then
    printf 'swarm'
  elif command -v docker >/dev/null 2>&1; then
    printf 'docker'
  else
    printf 'linux'
  fi
}

detect_k8s_role() {
  local units
  units="$(systemd_units)"
  if has_unit "$units" 'rke2-server'; then
    printf 'server'
  elif has_unit "$units" 'rke2-agent'; then
    printf 'agent'
  else
    printf 'other'
  fi
}

# Known services with an installed systemd unit, as a comma list.
detect_services() {
  local units found=""
  units="$(systemd_units)"
  add() { has_unit "$units" "$2" && found="${found:+${found},}$1" || true; }
  add nginx 'nginx'
  add apache 'apache2|httpd'
  add haproxy 'haproxy'
  add traefik 'traefik'
  add postfix 'postfix'
  add mysql 'mysql|mysqld'
  add mariadb 'mariadb'
  add postgresql 'postgresql'
  add redis 'redis|redis-server'
  printf '%s' "$found"
}

# cluster_name of collectors.<block> in the current config (best effort, for defaults).
existing_cluster_name() {
  [ -f "$CONFIG_FILE" ] || return 1
  awk -v block="  $1:" '
    $0 == block { inside = 1; next }
    inside && /^  [^ #]/ { exit }
    inside && /^    cluster_name:/ {
      sub(/^    cluster_name:[[:space:]]*/, ""); gsub(/["\047]/, ""); print; exit
    }
  ' "$CONFIG_FILE"
}

existing_env_token() {
  [ -f "$ENV_FILE" ] || return 1
  sed -n 's/^ARGUS_AGENT_TOKEN=//p' "$ENV_FILE" | head -n 1
}

platform_label() {
  case "$1" in
    auto) printf 'deteccao automatica' ;;
    linux) printf 'Linux (sem containers)' ;;
    docker) printf 'Docker standalone' ;;
    swarm) printf 'Docker Swarm' ;;
    k8s) printf 'Kubernetes' ;;
    proxmox) printf 'Proxmox VE' ;;
  esac
}

run_interactive() {
  local detected choice value existing_token detected_services i
  local -a platforms=(linux docker swarm k8s proxmox auto)
  local -a k8s_roles=(server agent other)

  tty_available || die "--interactive needs a terminal (or pass the options as flags)"

  say ""
  say "=== Instalacao do Argus Agent (${INSTALLER_VERSION}) ==="
  say "Enter aceita o valor entre colchetes. Nada e instalado antes da confirmacao final."
  say ""

  say "--- Servidor e identidade ---"
  ARGUS_SERVER_URL="$(ask "URL do Argus Server (API)" "$ARGUS_SERVER_URL")"
  ARGUS_INSTALL_BASE_URL="$(ask "URL de download do agent (release server)" "$ARGUS_INSTALL_BASE_URL")"
  if [ -z "$ARGUS_AGENT_TOKEN" ]; then
    existing_token="$(existing_env_token || true)"
    if [ -n "$existing_token" ] && ask_yes_no "Manter o token ja configurado neste host?" s; then
      ARGUS_AGENT_TOKEN="$existing_token"
    fi
  fi
  while [ -z "$ARGUS_AGENT_TOKEN" ]; do
    ARGUS_AGENT_TOKEN="$(ask_secret "Token do agent (nao aparece na tela)")"
  done
  value="$(read_agent_config_value project || true)"
  value="$(ask "Projeto" "${value:-$ARGUS_PROJECT}")"
  ARGUS_PROJECT="$value"
  ARGUS_PROJECT_EXPLICIT=1
  value="$(read_agent_config_value environment || true)"
  ARGUS_AGENT_ENVIRONMENT="$(ask "Ambiente (production, staging...)" "${value:-$ARGUS_AGENT_ENVIRONMENT}")"
  value="$(ask "Site (mudar depois troca o agent_id do host)" "$ARGUS_AGENT_SITE")"
  if [ "$value" != "$ARGUS_AGENT_SITE" ]; then
    ARGUS_AGENT_SITE="$value"
    ARGUS_AGENT_SITE_EXPLICIT=1
  fi

  say ""
  say "--- Tipo de host ---"
  detected="$(detect_platform)"
  i=1
  for value in "${platforms[@]}"; do
    [ "$value" = "$detected" ] && break
    i=$((i + 1))
  done
  choice="$(ask_choice "O que roda neste host? (detectado: $(platform_label "$detected"))" "$i" \
    "Linux (sem containers)" "Docker standalone" "Docker Swarm" "Kubernetes" "Proxmox VE" \
    "Detectar automaticamente")"
  ARGUS_PLATFORM="${platforms[$((choice - 1))]}"

  case "$ARGUS_PLATFORM" in
    swarm | k8s)
      value="docker_swarm"
      [ "$ARGUS_PLATFORM" = swarm ] || value="kubernetes"
      value="$(existing_cluster_name "$value" || true)"
      while true; do
        ARGUS_CLUSTER_NAME="$(ask "Nome do cluster (use o mesmo em todos os nos)" "${ARGUS_CLUSTER_NAME:-$value}")"
        [[ "$ARGUS_CLUSTER_NAME" =~ ^[A-Za-z0-9._-]{1,63}$ ]] && break
        say "  Use letras, numeros, '.', '_' ou '-' (ate 63)."
      done
      ;;
  esac
  if [ "$ARGUS_PLATFORM" = k8s ]; then
    case "$(detect_k8s_role)" in server) i=1 ;; agent) i=2 ;; *) i=3 ;; esac
    choice="$(ask_choice "Papel deste no no Kubernetes:" "$i" \
      "control-plane RKE2 (rke2-server)" "worker RKE2 (rke2-agent)" "outro (k3s, kubeadm, gerenciado)")"
    ARGUS_K8S_ROLE="${k8s_roles[$((choice - 1))]}"
  fi

  say ""
  say "--- Servicos monitorados ---"
  detected_services="$(detect_services)"
  say "Detectados neste host: ${detected_services:-nenhum}"
  choice="$(ask_choice "Como escolher os servicos?" 1 \
    "Detectar automaticamente (recomendado)" "Escolher da lista" "Nenhum")"
  case "$choice" in
    1) ARGUS_SERVICES=auto ;;
    3) ARGUS_SERVICES=none ;;
    2)
      say "Servicos conhecidos: $(join_by "${KNOWN_SERVICES[@]}")"
      while true; do
        ARGUS_SERVICES="$(ask "Lista separada por virgula" "${detected_services:-none}")"
        ARGUS_SERVICES="${ARGUS_SERVICES// /}"
        [ -n "$ARGUS_SERVICES" ] || ARGUS_SERVICES=none
        (validate_install_options) 2>/dev/null && break
        say "  Lista invalida. Use apenas: $(join_by "${KNOWN_SERVICES[@]}")"
      done
      ;;
  esac
  if [ "$(service_mode mysql)" = true ] &&
    ask_yes_no "Informar DSN do MySQL para checks de banco? (senao, so servico/porta)" n; then
    ARGUS_MYSQL_DSN="$(ask_secret "DSN MySQL (ex.: user:senha@tcp(127.0.0.1:3306)/)")"
  fi
  if [ "$(service_mode postgresql)" = true ] &&
    ask_yes_no "Informar DSN do PostgreSQL para checks de banco? (senao, so servico/porta)" n; then
    ARGUS_POSTGRES_DSN="$(ask_secret "DSN PostgreSQL (ex.: postgres://user:senha@127.0.0.1:5432/postgres)")"
  fi

  say ""
  say "--- Limpeza automatica ---"
  if [ "$ARGUS_PLATFORM" = linux ] || [ "$ARGUS_PLATFORM" = proxmox ] ||
    ! command -v docker >/dev/null 2>&1; then
    say "Sem Docker neste host: a limpeza automatica (Docker) nao se aplica. Apenas alertas."
    ARGUS_AUTO_CLEANUP=false
  else
    say "Quando o disco / passar de 90%, o agent pode limpar sozinho:"
    say "  imagens Docker sem uso, build cache, containers parados e logs de container > 100MB."
    say "  Nunca remove volumes nem itens com label argus.protect/backup/database."
    if ask_yes_no "Ativar limpeza automatica?" s; then
      ARGUS_AUTO_CLEANUP=true
    else
      ARGUS_AUTO_CLEANUP=false
    fi
  fi

  if [ -f "$CONFIG_FILE" ] && [ "$ARGUS_FORCE_CONFIG" != "true" ]; then
    say ""
    say "Ja existe ${CONFIG_FILE}. As respostas acima so valem se ele for substituido"
    say "(o atual vira backup ${CONFIG_FILE}.bak.<data>)."
    if ask_yes_no "Substituir a config existente?" s; then
      ARGUS_FORCE_CONFIG=true
    fi
  fi
}

print_install_plan() {
  local services="" name mode suffix
  suffix="$(arch_suffix)"
  resolve_platform_modes
  for name in "${KNOWN_SERVICES[@]}"; do
    mode="$(service_mode "$name")"
    case "$mode" in
      true) services="${services:+${services}, }${name}" ;;
      auto) services="${services:+${services}, }${name}?" ;;
    esac
  done

  say ""
  say "================ Resumo: o que sera executado neste host ================"
  say "Instalacao (agora):"
  say "  - baixar ${ARGUS_INSTALL_BASE_URL%/}/argus-agent-linux-${suffix} e validar o SHA256"
  say "  - instalar ${BIN_FILE}"
  say "  - gravar ${SERVICE_FILE} (root, prioridade de CPU baixa, max 512MB RAM)"
  say "  - gravar ${ENV_FILE} (0600: token${ARGUS_MYSQL_DSN:+, DSN MySQL}${ARGUS_POSTGRES_DSN:+, DSN PostgreSQL})"
  if [ -f "$CONFIG_FILE" ] && [ "$ARGUS_FORCE_CONFIG" != "true" ]; then
    say "  - MANTER ${CONFIG_FILE}; as respostas vao para ${CONFIG_FILE}.new (nao aplicadas)"
  elif [ -f "$CONFIG_FILE" ]; then
    say "  - substituir ${CONFIG_FILE} (backup ${CONFIG_FILE}.bak.<data>)"
  else
    say "  - gravar ${CONFIG_FILE}"
  fi
  say "  - systemctl enable + restart argus-agent"
  say ""
  say "Identidade: ${ARGUS_SERVER_URL} | projeto ${ARGUS_PROJECT} | ambiente ${ARGUS_AGENT_ENVIRONMENT} | site ${ARGUS_AGENT_SITE}"
  say ""
  say "O agent, a cada ${ARGUS_AGENT_INTERVAL_SECONDS}s (somente leitura):"
  say "  - host, CPU, memoria, disco, servicos systemd, top processos"
  say "  - descoberta local de servicos (sem varredura de rede)"
  say "  - plataforma: $(platform_label "$ARGUS_PLATFORM")"
  case "$ARGUS_PLATFORM" in
    docker) say "      containers travados, unhealthy, OOM, restart loop, CPU/memoria/PIDs" ;;
    swarm)
      say "      containers + cluster Swarm: nos, quorum, servicos, tasks presas/com falha"
      say "      nome do cluster: ${ARGUS_CLUSTER_NAME:-<id do cluster>}"
      ;;
    k8s)
      say "      Kubernetes em modo descoberta; RKE2: ${ARGUS_K8S_ROLE}"
      say "      nome do cluster: ${ARGUS_CLUSTER_NAME} (gravado para a visao de cluster K8s)"
      ;;
    proxmox) say "      VMs, LXC, storage, cluster e HA (pvesh/qm/pct, somente leitura)" ;;
    auto) say "      Docker/Swarm/Kubernetes/virtualizacao conforme o que for detectado" ;;
  esac
  say "  - servicos: ${services:-nenhum}  (? = so se detectado)"
  say ""
  say "Acoes que o agent pode executar no host:"
  say "  - systemctl reset-failed em units falhas (limpa o estado, nao reinicia nada)"
  case "$ARGUS_AUTO_CLEANUP" in
    true)
      say "  - limpeza ATIVADA: com / >= 90%, prune de imagens sem uso, build cache e containers"
      say "    parados; trunca logs de container > 100MB. Nunca volumes nem itens protegidos."
      say "  - journald vacuum (7 dias / 500MB) somente com aprovacao no dashboard"
      ;;
    false) say "  - limpeza automatica DESATIVADA: apenas alertas" ;;
    *) say "  - limpeza: nenhuma acao liberada neste host (padrao)" ;;
  esac
  say "  - nada alem disso: o Argus opera em modo recomendacao"
  say "========================================================================="
}

install_agent() {
  require_root
  require_cmd uname
  require_cmd systemctl
  [ "$ARGUS_WITH_APM" != "true" ] || ensure_tar

  preserve_existing_agent_identity
  # No token and a terminal: ask instead of failing.
  if [ "$ARGUS_INTERACTIVE" != "true" ] && [ -z "$ARGUS_AGENT_TOKEN" ] && tty_available; then
    ARGUS_INTERACTIVE=true
  fi
  if [ "$ARGUS_INTERACTIVE" = "true" ]; then
    run_interactive
    validate_install_options
    print_install_plan
    ask_yes_no "Confirmar e instalar?" s || die "instalacao cancelada; nada foi alterado"
  else
    validate_install_options
  fi

  if [ -z "$ARGUS_AGENT_TOKEN" ]; then
    die "ARGUS_AGENT_TOKEN is required"
  fi
  if [ -z "$ARGUS_SERVER_URL" ]; then
    die "ARGUS_SERVER_URL is required"
  fi

  local suffix binary_name binary_url checksum_url temp_file checksums_file
  suffix="$(arch_suffix)"
  binary_name="argus-agent-linux-${suffix}"
  binary_url="${ARGUS_INSTALL_BASE_URL%/}/${binary_name}"
  checksum_url="${ARGUS_INSTALL_BASE_URL%/}/SHA256SUMS"
  temp_file="$(mktemp)"
  checksums_file="$(mktemp)"
  trap "cleanup_temp_file '$temp_file'; cleanup_temp_file '$checksums_file'" EXIT

  log "downloading ${binary_url}"
  log "installer version ${INSTALLER_VERSION}"
  download_file "$binary_url" "$temp_file"
  log "downloading ${checksum_url}"
  download_file "$checksum_url" "$checksums_file"
  validate_checksum "$temp_file" "$binary_name" "$checksums_file"
  chmod 0755 "$temp_file"

  install -d "$CONFIG_DIR" "$SPOOL_DIR"
  install -m 0755 "$temp_file" "$BIN_FILE"
  ln -sfn "$BIN_FILE" "$LEGACY_CTL_FILE"
  cleanup_temp_file "$temp_file"
  cleanup_temp_file "$checksums_file"
  trap - EXIT
  write_service
  write_env
  fetch_profile_overlay
  if [ -n "$PROFILE_OVERLAY_FILE" ] && [ -f "$CONFIG_FILE" ] && [ "$ARGUS_FORCE_CONFIG" != "true" ]; then
    # Existing config + profile: merge in place instead of writing config.yml.new.
    update_existing_config_project
    apply_profile_overlay || write_config
  else
    write_config
    if [ -n "$PROFILE_OVERLAY_FILE" ]; then
      apply_profile_overlay || true
    fi
  fi
  cleanup_temp_file "$PROFILE_OVERLAY_FILE"
  if [ "$ARGUS_WITH_APM" = "true" ]; then
    install_apm
    enable_apm_in_config "$CONFIG_FILE"
    [ ! -f "${CONFIG_FILE}.new" ] || enable_apm_in_config "${CONFIG_FILE}.new"
  fi
  write_agent_activation_dropin

  systemctl daemon-reload
  systemctl enable argus-agent >/dev/null
  systemctl restart argus-agent

  log "installed and started argus-agent"
  log "config: ${CONFIG_FILE}"
}

# --- Argus APM (--with-apm) ---------------------------------------------------

write_apm_service() {
  cat >"$APM_SERVICE_FILE" <<'EOF'
[Unit]
Description=Argus APM (OpenTelemetry Collector + OBI)
Documentation=https://argus-homolog.eulabs.net.br/docs/
After=network-online.target
Wants=network-online.target
# The config is rendered by argus-agent from the server policy; the agent
# starts, reloads and stops this unit (and only this one).
ConditionPathExists=/etc/argus-apm/config.yaml
# Every restart re-attaches uprobes in all observed processes; a crash loop
# stalls the host (2026-10-22). Give up after 3 failures in 30 min.
StartLimitIntervalSec=30min
StartLimitBurst=3

[Service]
Type=simple
# The gate only allows a profiles pipeline; the agent renders one when the
# policy turns profiling on (apm-go-profiling). OBI's caps cover the profiler.
ExecStart=/usr/local/bin/argus-apm --config=/etc/argus-apm/config.yaml --feature-gates=service.profilesSupport
# The Collector reloads its config on SIGHUP.
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
User=root
# Fail early naming the missing capability instead of a verifier log (D10).
Environment=OTEL_EBPF_ENFORCE_SYS_CAPS=true
# Application observability (design D10). Context propagation adds
# CAP_NET_ADMIN (+ CAP_SYS_ADMIN for Go): installer --apm-propagation writes
# argus-apm.service.d/10-propagation.conf. The agent reports the effective
# set and marks missing features "degraded".
# CAP_SYSLOG: the CPU profiler reads /proc/kallsyms addresses, which the
# kernel hides without it (perf_event_paranoid > 1).
CapabilityBoundingSet=CAP_BPF CAP_PERFMON CAP_DAC_READ_SEARCH CAP_CHECKPOINT_RESTORE CAP_SYS_PTRACE CAP_NET_RAW CAP_SYSLOG
AmbientCapabilities=CAP_BPF CAP_PERFMON CAP_DAC_READ_SEARCH CAP_CHECKPOINT_RESTORE CAP_SYS_PTRACE CAP_NET_RAW CAP_SYSLOG
# Kernels < 5.11 charge BPF maps to RLIMIT_MEMLOCK: OBI raises it and, without
# CAP_SYS_RESOURCE, failed with EPERM and traced nothing (PenseAPP, 5.10).
LimitMEMLOCK=infinity
NoNewPrivileges=true
# Read-only host; writes only the disk queue. /sys stays readable for BTF
# and tracefs (no ProtectKernelTunables / ProtectControlGroups on purpose).
ProtectSystem=strict
ReadWritePaths=/var/lib/argus-apm
ProtectHome=true
PrivateTmp=true
ProtectKernelModules=true
ProtectClock=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
# memory_limiter is 384 MiB by default; the unit is the hard stop. The
# cgroup also counts the OBI BPF maps (~350 MiB on a busy Swarm node): at
# 448M the kernel throttled argus-apm until health/export stalled (2026-10-22).
MemoryHigh=1G
MemoryMax=1536M
CPUQuota=50%
CPUWeight=50
# OBI closes every uprobe link in parallel on reload; each blocking close()
# costs an OS thread, and 512 crashed hosts with ~14 services (newosproc).
TasksMax=4096

[Install]
WantedBy=multi-user.target
EOF
  if [ "$ARGUS_APM_PROPAGATION" = "true" ]; then
    install -d -m 0755 "$APM_DROPIN_DIR"
    cat >"${APM_DROPIN_DIR}/10-propagation.conf" <<'EOF'
# Context propagation (installer --apm-propagation): CAP_NET_ADMIN for
# headers/tcp, CAP_SYS_ADMIN for the Go uprobe injection (design D10).
[Service]
CapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_ADMIN
AmbientCapabilities=CAP_NET_ADMIN CAP_SYS_ADMIN
EOF
  else
    rm -f "${APM_DROPIN_DIR}/10-propagation.conf"
    rmdir "$APM_DROPIN_DIR" >/dev/null 2>&1 || true
  fi
}

enable_apm_in_config() {
  local target="$1"
  [ -f "$target" ] || return 0
  if grep -Eq '^apm:' "$target"; then
    log "apm block already present in ${target}; left as is"
    return 0
  fi
  local runtimes_url="" actions="[]"
  case "$ARGUS_INSTALL_BASE_URL" in
    https://*) runtimes_url="${ARGUS_INSTALL_BASE_URL%/}/apm-runtimes" ;;
  esac
  [ "$ARGUS_APM_ACTIVATION" = "true" ] && actions="[systemd_dropin]"
  cat >>"$target" <<EOF

# Argus APM (installer --with-apm). The server policy decides what runs;
# this block can only restrict it (see docs/agent.md).
apm:
  enabled: true
  instrumentation:
    # systemd_dropin: write the drop-in of approved level-2 plans (never restart).
    allowed_actions: ${actions}
  runtimes:
    # Level-2 bundles, verified against the signed manifest.
    base_url: "${runtimes_url}"
EOF
}

# The agent unit keeps /etc read-only (ProtectSystem=full); writing an
# application's drop-in needs /etc/systemd/system, only with the opt-in.
write_agent_activation_dropin() {
  if [ "$ARGUS_APM_ACTIVATION" = "true" ]; then
    install -d -m 0755 "$(dirname "$AGENT_ACTIVATION_DROPIN")"
    cat >"$AGENT_ACTIVATION_DROPIN" <<'EOF'
# Argus APM level-2 activation (installer --allow-apm-activation): the agent
# writes /etc/systemd/system/<unit>.d/90-argus-apm.conf of approved plans and
# runs `systemctl daemon-reload`. It never restarts the application.
[Service]
ReadWritePaths=/etc/systemd/system
EOF
    log "argus-agent may write Argus APM drop-ins (--allow-apm-activation)"
  else
    rm -f "$AGENT_ACTIVATION_DROPIN"
    rmdir "$(dirname "$AGENT_ACTIVATION_DROPIN")" >/dev/null 2>&1 || true
  fi
}

install_apm() {
  local suffix tarball url checksum_url temp_tar checksums_file extract_dir
  suffix="$(arch_suffix)"
  tarball="argus-apm-linux-${suffix}.tar.gz"
  url="${ARGUS_INSTALL_BASE_URL%/}/${tarball}"
  checksum_url="${ARGUS_INSTALL_BASE_URL%/}/SHA256SUMS"
  temp_tar="$(mktemp)"
  checksums_file="$(mktemp)"
  extract_dir="$(mktemp -d)"

  log "downloading ${url}"
  download_file "$url" "$temp_tar"
  download_file "$checksum_url" "$checksums_file"
  validate_checksum "$temp_tar" "$tarball" "$checksums_file"
  tar -xzf "$temp_tar" -C "$extract_dir"
  [ -x "${extract_dir}/argus-apm" ] || die "argus-apm binary missing from ${tarball}"
  [ -f "${extract_dir}/share/doc/argus-apm/NOTICE" ] || die "NOTICE missing from ${tarball}"

  install -m 0755 "${extract_dir}/argus-apm" "$APM_BIN_FILE"
  install -d -m 0755 /usr/local/share/doc/argus-apm
  cp -R "${extract_dir}/share/doc/argus-apm/." /usr/local/share/doc/argus-apm/
  install -d -m 0750 "$APM_CONFIG_DIR"
  install -d -m 0750 "$APM_STATE_DIR"
  # The SiteToken the node presents to the gateway (bearertokenauth reads
  # the file). The global token is refused by the gateway.
  (umask 077 && printf '%s' "$ARGUS_AGENT_TOKEN" >"${APM_CONFIG_DIR}/token")
  chmod 0600 "${APM_CONFIG_DIR}/token"
  write_apm_service
  rm -rf "$extract_dir"
  cleanup_temp_file "$temp_tar"
  cleanup_temp_file "$checksums_file"
  log "installed argus-apm (started by the agent when the site policy enables APM)"
}

uninstall_apm() {
  rm -f "$AGENT_ACTIVATION_DROPIN"
  [ -f "$APM_SERVICE_FILE" ] || [ -x "$APM_BIN_FILE" ] || return 0
  systemctl disable --now argus-apm >/dev/null 2>&1 || true
  rm -f "$APM_SERVICE_FILE" "$APM_BIN_FILE"
  rm -rf "$APM_DROPIN_DIR" "$APM_CONFIG_DIR" /usr/local/share/doc/argus-apm
  if [ "$ARGUS_PURGE_SPOOL" = "true" ]; then
    rm -rf "$APM_STATE_DIR"
  fi
  log "removed argus-apm"
}

uninstall_agent() {
  require_root
  require_cmd systemctl

  systemctl disable --now argus-agent >/dev/null 2>&1 || true
  uninstall_apm
  rm -f "$SERVICE_FILE"
  rm -f "$BIN_FILE"
  rm -f "$LEGACY_CTL_FILE"
  rm -f "$CONFIG_FILE"
  rm -f "$ENV_FILE"
  rmdir "$CONFIG_DIR" >/dev/null 2>&1 || true

  if [ "$ARGUS_PURGE_SPOOL" = "true" ]; then
    rm -rf /var/lib/argus-agent
  fi

  systemctl daemon-reload
  log "removed argus-agent service, binary, env file and config file"
}

main() {
  parse_args "$@"
  case "$COMMAND" in
    install)
      install_agent
      ;;
    uninstall | remove)
      uninstall_agent
      ;;
    -h | --help | help)
      usage
      ;;
    *)
      usage
      die "unknown command: $COMMAND"
      ;;
  esac
}

main "$@"
