#!/bin/bash
# Arts Discovery Agent - Linux
# Collects system information and POSTs to Arts server
# (c) Adrian Ranta. All rights reserved. Unauthorised use, reproduction and or distribution is strictly prohibited.

# Wrap entire script in a block so bash reads it all into memory before executing.
# This prevents corruption when the script self-updates (overwrites itself on disk).
{

set -euo pipefail

AGENT_VERSION="2.0.0"
SCRIPT_PATH="$(readlink -f "$0")"
ARTS_SERVER="${ARTS_SERVER:-http://192.168.1.207}"
ARTS_API_KEY="${ARTS_API_KEY:-}"
CONFIG_FILE="/etc/arts/agent.conf"

if [ -f "$CONFIG_FILE" ]; then
  source "$CONFIG_FILE"
fi

if [ -z "$ARTS_API_KEY" ]; then
  echo "ERROR: ARTS_API_KEY not set. Set it in $CONFIG_FILE or environment." >&2
  exit 1
fi

# Detect virtualisation: bare-metal | vm | container
PLATFORM_TYPE="bare-metal"
VIRT_ENGINE=""

detect_platform() {
  # LXC / OpenVZ container
  if [ -f /run/container_type ]; then
    PLATFORM_TYPE="container"
    VIRT_ENGINE=$(cat /run/container_type 2>/dev/null)
    return
  fi
  if grep -qsE ':/lxc/' /proc/1/cgroup 2>/dev/null; then
    PLATFORM_TYPE="container"
    VIRT_ENGINE="lxc"
    return
  fi
  if [ -f /.dockerenv ] || grep -qs 'docker\|containerd' /proc/1/cgroup 2>/dev/null; then
    PLATFORM_TYPE="container"
    VIRT_ENGINE="docker"
    return
  fi
  if grep -qs 'container=lxc' /proc/1/environ 2>/dev/null; then
    PLATFORM_TYPE="container"
    VIRT_ENGINE="lxc"
    return
  fi

  # VM detection via DMI / hypervisor
  local product
  product=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "")
  local sys_vendor
  sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "")
  local bios_vendor
  bios_vendor=$(cat /sys/class/dmi/id/bios_vendor 2>/dev/null || echo "")

  case "$product" in
    *"Virtual Machine"*|*"VirtualBox"*) PLATFORM_TYPE="vm"; VIRT_ENGINE="hyperv-or-vbox" ;;
    *"KVM"*|*"QEMU"*|*"Standard PC"*) PLATFORM_TYPE="vm"; VIRT_ENGINE="kvm" ;;
    *"VMware"*)                        PLATFORM_TYPE="vm"; VIRT_ENGINE="vmware" ;;
  esac
  [ "$PLATFORM_TYPE" != "bare-metal" ] && return

  case "$sys_vendor" in
    *"QEMU"*|*"Bochs"*)  PLATFORM_TYPE="vm"; VIRT_ENGINE="kvm" ;;
    *"VMware"*)           PLATFORM_TYPE="vm"; VIRT_ENGINE="vmware" ;;
    *"Xen"*)              PLATFORM_TYPE="vm"; VIRT_ENGINE="xen" ;;
    *"Microsoft"*"Hyper"*) PLATFORM_TYPE="vm"; VIRT_ENGINE="hyperv" ;;
    *"innotek"*|*"Oracle"*) PLATFORM_TYPE="vm"; VIRT_ENGINE="virtualbox" ;;
  esac
  [ "$PLATFORM_TYPE" != "bare-metal" ] && return

  case "$bios_vendor" in
    *"SeaBIOS"*|*"QEMU"*) PLATFORM_TYPE="vm"; VIRT_ENGINE="kvm" ;;
  esac

  # systemd-detect-virt fallback
  if [ "$PLATFORM_TYPE" = "bare-metal" ] && command -v systemd-detect-virt >/dev/null 2>&1; then
    local virt
    virt=$(systemd-detect-virt 2>/dev/null || true)
    virt=$(echo "$virt" | head -1)
    case "$virt" in
      none|"") ;;
      lxc|lxc-libvirt|openvz|docker|podman|systemd-nspawn)
        PLATFORM_TYPE="container"; VIRT_ENGINE="$virt" ;;
      *)
        PLATFORM_TYPE="vm"; VIRT_ENGINE="$virt" ;;
    esac
  fi
}

detect_platform

collect_hostname() {
  hostname -f 2>/dev/null || hostname
}

collect_ip() {
  hostname -I 2>/dev/null | awk '{print $1}'
}

collect_mac() {
  local iface
  iface=$(ip route show default 2>/dev/null | awk '/default/ {print $5}' | head -1)
  if [ -n "$iface" ] && [ -f "/sys/class/net/${iface}/address" ]; then
    cat "/sys/class/net/${iface}/address"
  else
    ip link show 2>/dev/null | awk '/ether/ {print $2; exit}'
  fi
}

collect_os() {
  if [ -f /etc/os-release ]; then
    . /etc/os-release
    echo "${PRETTY_NAME:-${NAME} ${VERSION}}"
  else
    uname -s -r
  fi
}

collect_processor() {
  if [ "$PLATFORM_TYPE" = "container" ] || [ "$PLATFORM_TYPE" = "vm" ]; then
    local vcpus
    vcpus=$(nproc 2>/dev/null || echo "1")
    local host_type="Proxmox"
    [ "$PLATFORM_TYPE" = "container" ] && host_type="Proxmox"
    echo "${host_type} vCPU x${vcpus}"
  else
    grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | xargs
  fi
}

collect_memory_gb() {
  if [ "$PLATFORM_TYPE" = "bare-metal" ] || [ "$PLATFORM_TYPE" = "vm" ]; then
    # Use dmidecode to get actual installed RAM (not usable after kernel reservations)
    local installed
    installed=$(dmidecode -t 17 2>/dev/null | awk '/^\tSize:/ && /[0-9]/ {
      if (/GB/) {gsub(/[^0-9]/,"",$2); sum+=$2}
      else if (/MB/) {gsub(/[^0-9]/,"",$2); sum+=$2/1024}
    } END {if(sum>0) printf "%.0f", sum}')
    if [ -n "$installed" ] && [ "$installed" != "0" ]; then
      echo "$installed"
      return
    fi
  fi
  # Fallback (containers, or dmidecode unavailable): use /proc/meminfo
  awk '/MemTotal/ {printf "%.1f", $2/1048576}' /proc/meminfo 2>/dev/null
}

collect_storage_gb() {
  if [ "$PLATFORM_TYPE" = "container" ]; then
    # Report the container's allocated rootfs size
    df -B1 / 2>/dev/null | awk 'NR==2 {printf "%.0f", $2/1073741824}'
  else
    lsblk -bnd -o SIZE 2>/dev/null | awk '{s+=$1} END {printf "%.0f", s/1073741824}'
  fi
}

collect_serial() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/product_serial 2>/dev/null || echo ""
}

collect_make() {
  if [ "$PLATFORM_TYPE" = "container" ] || [ "$PLATFORM_TYPE" = "vm" ]; then
    echo "Proxmox"
  else
    cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo ""
  fi
}

collect_model() {
  if [ "$PLATFORM_TYPE" = "container" ]; then
    echo "LXC Container"
  elif [ "$PLATFORM_TYPE" = "vm" ]; then
    echo "Virtual Machine"
  else
    cat /sys/class/dmi/id/product_name 2>/dev/null || echo ""
  fi
}

collect_firmware() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/bios_version 2>/dev/null || echo ""
}

collect_bios_vendor() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/bios_vendor 2>/dev/null || echo ""
}

collect_bios_date() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/bios_date 2>/dev/null || echo ""
}

collect_board_vendor() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/board_vendor 2>/dev/null || echo ""
}

collect_board_name() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/board_name 2>/dev/null || echo ""
}

collect_board_serial() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/board_serial 2>/dev/null || echo ""
}

collect_chassis_type() {
  [ "$PLATFORM_TYPE" = "container" ] && echo "" && return
  cat /sys/class/dmi/id/chassis_type 2>/dev/null || echo ""
}

collect_network_interfaces() {
  local json=""
  local first=1
  for iface in /sys/class/net/*/; do
    iface=$(basename "$iface")
    [ "$iface" = "lo" ] && continue
    local mac=$(cat "/sys/class/net/${iface}/address" 2>/dev/null || echo "")
    [ -z "$mac" ] || [ "$mac" = "00:00:00:00:00:00" ] && continue
    local state=$(cat "/sys/class/net/${iface}/operstate" 2>/dev/null || echo "unknown")
    local speed=$(cat "/sys/class/net/${iface}/speed" 2>/dev/null || echo "")
    local ip=$(ip -4 addr show "$iface" 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -1)
    [ $first -eq 0 ] && json="$json,"
    json="$json{\"name\":\"$iface\",\"mac\":\"$mac\",\"state\":\"$state\""
    [ -n "$speed" ] && [ "$speed" != "-1" ] && json="$json,\"speed_mbps\":$speed"
    [ -n "$ip" ] && json="$json,\"ip\":\"$ip\""
    json="$json}"
    first=0
  done
  echo "$json"
}

collect_cpu_percent() {
  top -bn1 2>/dev/null | awk '/^%Cpu/ {printf "%.1f", 100-$8}' || echo ""
}

collect_disk_percent() {
  df / 2>/dev/null | awk 'NR==2 {gsub(/%/,""); print $5}'
}

collect_uptime_seconds() {
  awk '{printf "%.0f", $1}' /proc/uptime 2>/dev/null
}

collect_open_ports() {
  ss -tlnH 2>/dev/null | awk '{split($4, a, ":"); print a[length(a)]}' | sort -un | paste -sd,
}

collect_script_hash() {
  sha256sum "$SCRIPT_PATH" 2>/dev/null | awk '{print $1}'
}

collect_software_inventory() {
  local json=""
  local first=1
  local count=0
  local max=500

  # Sanitise: strip control characters and escape double quotes for valid JSON
  _sanitise() { tr -d '\000-\011\013-\037' | sed 's/\\/\\\\/g; s/"/\\"/g'; }

  if command -v dpkg-query >/dev/null 2>&1; then
    while IFS=$'\t' read -r name version; do
      [ $count -ge $max ] && break
      [ -z "$name" ] && continue
      name=$(printf '%s' "$name" | _sanitise)
      version=$(printf '%s' "$version" | _sanitise)
      [ -z "$name" ] && continue
      [ $first -eq 0 ] && json="$json,"
      json="$json{\"name\":\"$name\",\"version\":\"$version\"}"
      first=0
      count=$((count + 1))
    done < <(dpkg-query -W -f '${Package}\t${Version}\n' 2>/dev/null | sort)
  elif command -v rpm >/dev/null 2>&1; then
    while IFS=$'\t' read -r name version; do
      [ $count -ge $max ] && break
      [ -z "$name" ] && continue
      name=$(printf '%s' "$name" | _sanitise)
      version=$(printf '%s' "$version" | _sanitise)
      [ -z "$name" ] && continue
      [ $first -eq 0 ] && json="$json,"
      json="$json{\"name\":\"$name\",\"version\":\"$version\"}"
      first=0
      count=$((count + 1))
    done < <(rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' 2>/dev/null | sort)
  fi

  echo "$json"
}

collect_software_usage() {
  local state_dir="/var/lib/arts"
  local state_file="$state_dir/usage-state.tsv"
  local now
  now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  mkdir -p "$state_dir" 2>/dev/null

  # System process blocklist pattern
  local skip_pattern='^(systemd|kthreadd|init|sshd|cron|agetty|dbus-daemon|polkitd|rsyslogd|journald|udevd|networkd|resolved|timesyncd|logind|bash|sh|dash|sudo|login|su|screen|tmux|containerd|dockerd|runc|snapd|multipathd|irqbalance|accounts-daemon|at-spi|gdm|lightdm|gnome-shell|Xorg|Xwayland|pipewire|pulseaudio|wireplumber|dconf|gvfs|xdg-|ibus|colord|switcheroo|power-profiles|udisksd|packagekitd|fwupd|thermald|bolt|cupsd|avahi|ModemManager|wpa_supplicant|nm-|NetworkManager)$'

  # Collect running processes: exe_path -> name, memory_kb
  declare -A running_exes
  declare -A running_mem
  for pid_dir in /proc/[0-9]*/; do
    local pid="${pid_dir#/proc/}"
    pid="${pid%/}"
    local exe_path
    exe_path=$(readlink -f "${pid_dir}exe" 2>/dev/null) || continue
    [ -z "$exe_path" ] && continue
    [ ! -f "$exe_path" ] && continue

    # Skip system paths
    case "$exe_path" in
      /usr/lib/systemd/*|/lib/systemd/*|/sbin/*|/usr/sbin/*) continue ;;
    esac

    local exe_name
    exe_name=$(basename "$exe_path")
    # Skip system processes
    if echo "$exe_name" | grep -qE "$skip_pattern"; then
      continue
    fi

    if [ -z "${running_exes[$exe_path]+x}" ]; then
      # Get package name
      local pkg_name=""
      if command -v dpkg >/dev/null 2>&1; then
        pkg_name=$(dpkg -S "$exe_path" 2>/dev/null | head -1 | cut -d: -f1)
      elif command -v rpm >/dev/null 2>&1; then
        pkg_name=$(rpm -qf "$exe_path" 2>/dev/null | head -1)
        [ "$pkg_name" = "file $exe_path is not owned by any package" ] && pkg_name=""
      fi
      running_exes[$exe_path]="${pkg_name:-$exe_name}"
      # Memory in kB from VmRSS
      local mem_kb
      mem_kb=$(awk '/^VmRSS:/ {print $2}' "/proc/$pid/status" 2>/dev/null)
      running_mem[$exe_path]="${mem_kb:-0}"
    else
      # Accumulate memory for multiple instances
      local add_mem
      add_mem=$(awk '/^VmRSS:/ {print $2}' "/proc/$pid/status" 2>/dev/null)
      running_mem[$exe_path]=$(( ${running_mem[$exe_path]} + ${add_mem:-0} ))
    fi
  done

  # Load existing state (TSV: exe_path \t name \t first_seen \t last_seen \t run_count)
  declare -A state_name state_first state_last state_count
  if [ -f "$state_file" ]; then
    while IFS=$'\t' read -r s_path s_name s_first s_last s_count; do
      [ -z "$s_path" ] && continue
      state_name[$s_path]="$s_name"
      state_first[$s_path]="$s_first"
      state_last[$s_path]="$s_last"
      state_count[$s_path]="$s_count"
    done < "$state_file"
  fi

  # Update state with currently running processes
  for exe_path in "${!running_exes[@]}"; do
    local name="${running_exes[$exe_path]}"
    if [ -n "${state_count[$exe_path]+x}" ]; then
      state_last[$exe_path]="$now"
      state_count[$exe_path]=$(( ${state_count[$exe_path]} + 1 ))
      state_name[$exe_path]="$name"
    else
      state_name[$exe_path]="$name"
      state_first[$exe_path]="$now"
      state_last[$exe_path]="$now"
      state_count[$exe_path]=1
    fi
  done

  # Build sorted output (by run_count desc), cap at 200
  # Create temp file for sorting
  local tmpfile
  tmpfile=$(mktemp /tmp/arts-usage.XXXXXX)
  for exe_path in "${!state_count[@]}"; do
    printf '%d\t%s\n' "${state_count[$exe_path]}" "$exe_path" >> "$tmpfile"
  done
  local top_paths
  top_paths=$(sort -t$'\t' -k1 -rn "$tmpfile" | head -200 | cut -f2)
  rm -f "$tmpfile"

  # Output JSON and save state
  local json=""
  local first=1
  > "$state_file" 2>/dev/null  # truncate state file for rewrite
  while IFS= read -r exe_path; do
    [ -z "$exe_path" ] && continue
    local name="${state_name[$exe_path]}"
    local fs="${state_first[$exe_path]}"
    local ls="${state_last[$exe_path]}"
    local cnt="${state_count[$exe_path]}"
    local is_running="false"
    local mem_mb="null"
    if [ -n "${running_exes[$exe_path]+x}" ]; then
      is_running="true"
      local mem_kb="${running_mem[$exe_path]}"
      mem_mb=$(( (mem_kb + 512) / 1024 ))
    fi

    # Save to state file
    printf '%s\t%s\t%s\t%s\t%s\n' "$exe_path" "$name" "$fs" "$ls" "$cnt" >> "$state_file" 2>/dev/null

    # Sanitise for JSON
    name=$(printf '%s' "$name" | sed 's/\\/\\\\/g; s/"/\\"/g')
    exe_path_esc=$(printf '%s' "$exe_path" | sed 's/\\/\\\\/g; s/"/\\"/g')

    [ $first -eq 0 ] && json="$json,"
    json="$json{\"name\":\"$name\",\"exe_path\":\"$exe_path_esc\",\"is_running\":$is_running,\"first_seen\":\"$fs\",\"last_seen\":\"$ls\",\"run_count\":$cnt,\"memory_mb\":$mem_mb}"
    first=0
  done <<< "$top_paths"

  echo "$json"
}

collect_gpus() {
  # nvidia-smi preferred (gives driver + memory)
  if command -v nvidia-smi >/dev/null 2>&1; then
    local nv
    nv=$(nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader 2>/dev/null | awk -F', ' '
      {
        name=$1; drv=$2; mem=$3
        gsub(/"/, "\\\"", name); gsub(/ MiB$/, "", mem)
        if(n++) printf ","
        printf "{\"name\":\"%s\",\"driver\":\"%s\"", name, drv
        if(mem+0>0) printf ",\"ram_mb\":%d", mem
        printf "}"
      }')
    [ -n "$nv" ] && echo "$nv" && return
  fi
  # Fallback: lspci
  if command -v lspci >/dev/null 2>&1; then
    lspci 2>/dev/null | grep -iE 'vga|3d controller|display controller' | sed 's/^[^ ]* //' | awk '
      { gsub(/\\/, "\\\\"); gsub(/"/, "\\\"")
        if(n++) printf ","
        printf "{\"name\":\"%s\"}", $0
      }'
  fi
}

collect_ram_slots() {
  [ "$PLATFORM_TYPE" = "container" ] && return
  command -v dmidecode >/dev/null 2>&1 || return
  dmidecode -t memory 2>/dev/null | awk '
    /^Memory Device$/ { flush(); in_dev=1; slot=""; sz=""; spd=""; tp=""; pn=""; mf=""; next }
    !in_dev { next }
    /^\t?$/ || /^Handle/ { flush(); in_dev=0; next }
    /Locator:/ && !/Bank/ { sub(/.*Locator: */, ""); slot=$0 }
    /^\tSize:/ { sub(/.*Size: */, ""); sz=$0 }
    /^\tSpeed:/ && spd=="" { sub(/.*Speed: */, ""); spd=$0 }
    /^\tType:/ && !/Detail/ && tp=="" { sub(/.*Type: */, ""); tp=$0 }
    /Part Number:/ { sub(/.*Part Number: */, ""); gsub(/^ +| +$/, ""); pn=$0 }
    /Manufacturer:/ { sub(/.*Manufacturer: */, ""); gsub(/^ +| +$/, ""); mf=$0 }
    END { flush() }
    function flush() {
      if (!slot || !sz || sz=="No Module Installed") return
      split(sz, sa, " ")
      cap=sa[1]+0; if(sa[2]=="MB") cap=cap/1024
      if(cap<=0) return
      if(n++) printf ","
      printf "{\"slot\":\"%s\",\"capacity_gb\":%.1f", esc(slot), cap
      if(spd && spd!="Unknown") { gsub(/[^0-9]/, "", spd); if(spd+0>0) printf ",\"speed_mhz\":%s", spd }
      if(tp && tp!="Unknown") printf ",\"type\":\"%s\"", esc(tp)
      if(pn && pn!="Not Specified" && pn!="Unknown") printf ",\"part_number\":\"%s\"", esc(pn)
      if(mf && mf!="Not Specified" && mf!="Unknown") printf ",\"manufacturer\":\"%s\"", esc(mf)
      printf "}"
    }
    function esc(s) { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); return s }
  '
}

collect_disks() {
  [ "$PLATFORM_TYPE" = "container" ] && return
  local n=0
  while IFS= read -r line; do
    echo "$line" | grep -q 'TYPE="disk"' || continue
    local size=$(echo "$line" | grep -o 'SIZE="[^"]*"' | cut -d'"' -f2)
    [ -z "$size" ] || [ "$size" = "0" ] && continue
    local size_gb=$((size / 1073741824))
    [ "$size_gb" -eq 0 ] && continue
    local model=$(echo "$line" | grep -o 'MODEL="[^"]*"' | cut -d'"' -f2 | xargs)
    local serial=$(echo "$line" | grep -o 'SERIAL="[^"]*"' | cut -d'"' -f2 | xargs)
    local rota=$(echo "$line" | grep -o 'ROTA="[^"]*"' | cut -d'"' -f2)
    local tran=$(echo "$line" | grep -o 'TRAN="[^"]*"' | cut -d'"' -f2)
    local media="HDD"; [ "$rota" = "0" ] && media="SSD"
    model=$(printf '%s' "$model" | sed 's/\\/\\\\/g; s/"/\\"/g')
    serial=$(printf '%s' "$serial" | sed 's/\\/\\\\/g; s/"/\\"/g')
    [ $n -gt 0 ] && printf ","
    printf '{"model":"%s","size_gb":%d,"media_type":"%s"' "$model" "$size_gb" "$media"
    [ -n "$serial" ] && printf ',"serial":"%s"' "$serial"
    [ -n "$tran" ] && printf ',"interface":"%s"' "$tran"
    printf '}'
    n=$((n + 1))
  done < <(lsblk -dPb -o SIZE,MODEL,SERIAL,ROTA,TRAN,TYPE 2>/dev/null)
}

collect_volumes() {
  df -Tk 2>/dev/null | awk '
    NR>1 && $1~/^\/dev/ {
      mount=$7; for(i=8;i<=NF;i++) mount=mount" "$i
      gsub(/\\/, "\\\\", mount); gsub(/"/, "\\\"", mount)
      if(n++) printf ","
      printf "{\"mount\":\"%s\",\"size_gb\":%.1f,\"free_gb\":%.1f,\"file_system\":\"%s\"}", mount, $3/1048576, $5/1048576, $2
    }'
}

collect_cpu_cores() {
  local cores sockets
  cores=$(lscpu 2>/dev/null | awk -F: '/Core\(s\) per socket/ {gsub(/[^0-9]/, "", $2); print $2}')
  sockets=$(lscpu 2>/dev/null | awk -F: '/^Socket\(s\)/ {gsub(/[^0-9]/, "", $2); print $2}')
  if [ -n "$cores" ] && [ -n "$sockets" ]; then
    echo $((cores * sockets))
  fi
}

collect_cpu_threads() {
  nproc 2>/dev/null
}

collect_cpu_max_mhz() {
  local mhz
  mhz=$(lscpu 2>/dev/null | awk -F: '/CPU max MHz/ {gsub(/^ +| +$/, "", $2); printf "%.0f", $2}')
  [ -n "$mhz" ] && [ "$mhz" != "0" ] && echo "$mhz" && return
  awk -F: '/cpu MHz/ {printf "%.0f", $2; exit}' /proc/cpuinfo 2>/dev/null
}

collect_os_install_date() {
  local btime
  btime=$(stat -c %W / 2>/dev/null)
  if [ -n "$btime" ] && [ "$btime" != "0" ]; then
    date -d "@$btime" +%Y-%m-%d 2>/dev/null && return
  fi
  if [ -f /etc/machine-id ]; then
    btime=$(stat -c %W /etc/machine-id 2>/dev/null)
    if [ -n "$btime" ] && [ "$btime" != "0" ]; then
      date -d "@$btime" +%Y-%m-%d 2>/dev/null && return
    fi
  fi
}

collect_last_boot() {
  who -b 2>/dev/null | awk '{print $(NF-1)" "$NF}'
}

collect_domain() {
  if command -v realm >/dev/null 2>&1; then
    local d
    d=$(realm list --name-only 2>/dev/null | head -1)
    [ -n "$d" ] && echo "$d" && return
  fi
  local hd
  hd=$(hostname -d 2>/dev/null)
  [ -n "$hd" ] && [ "$hd" != "(none)" ] && echo "$hd" && return
  awk '/^domain/ {print $2; exit}' /etc/resolv.conf 2>/dev/null
}

collect_logged_in_users() {
  who 2>/dev/null | awk '
    { gsub(/"/, "\\\"", $1)
      if(n++) printf ","
      printf "{\"user\":\"%s\",\"tty\":\"%s\",\"since\":\"%s %s\"}", $1, $2, $3, $4
    }'
}

collect_services() {
  command -v systemctl >/dev/null 2>&1 || return
  systemctl list-units --type=service --state=running --no-legend --no-pager 2>/dev/null | awk '
    NR<=200 {
      sub(/\.service.*/, "", $1); gsub(/"/, "\\\"", $1)
      if(n++) printf ","
      printf "\"%s\"", $1
    }'
}

collect_monitors() {
  command -v xrandr >/dev/null 2>&1 || return
  xrandr --query 2>/dev/null | awk '
    / connected/ {
      split($0, a, " connected")
      name=a[1]; gsub(/^ +| +$/, "", name)
      res=""
      while ((getline line < "/dev/stdin") > 0) {
        if (line ~ /^[A-Za-z]/) break
        if (line ~ /\*/) { sub(/^ +/, "", line); split(line, r, " "); res=r[1]; break }
      }
      gsub(/"/, "\\\"", name)
      if(n++) printf ","
      printf "{\"name\":\"%s\"", name
      if(res) printf ",\"resolution\":\"%s\"", res
      printf "}"
    }'
}

collect_docker_containers() {
  local cmd=""
  if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
    cmd="docker"
  elif command -v podman >/dev/null 2>&1; then
    cmd="podman"
  else
    return
  fi
  $cmd ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null | awk -F'\t' '
    NR<=100 {
      gsub(/"/, "\\\"", $1); gsub(/"/, "\\\"", $2); gsub(/"/, "\\\"", $3)
      if(n++) printf ","
      printf "{\"name\":\"%s\",\"image\":\"%s\",\"status\":\"%s\"}", $1, $2, $3
    }'
}

collect_battery() {
  command -v upower >/dev/null 2>&1 || return
  local bat
  bat=$(upower -e 2>/dev/null | grep battery | head -1)
  [ -z "$bat" ] && return
  upower -i "$bat" 2>/dev/null | awk '
    /state:/ { sub(/.*state: */, ""); state=$0 }
    /percentage:/ { sub(/.*percentage: */, ""); pct=$0; gsub(/%/, "", pct) }
    /energy-full-design:/ { sub(/.*energy-full-design: */, ""); design=$0 }
    /energy-full:/ { sub(/.*energy-full: */, ""); full=$0 }
    /technology:/ { sub(/.*technology: */, ""); tech=$0 }
    END {
      if(!state && !pct) exit
      gsub(/^ +| +$/, "", state); gsub(/^ +| +$/, "", pct)
      printf "{\"state\":\"%s\",\"percentage\":\"%s\"", state, pct
      if(design) { gsub(/^ +| +$/, "", design); printf ",\"design_capacity\":\"%s\"", design }
      if(full) { gsub(/^ +| +$/, "", full); printf ",\"full_capacity\":\"%s\"", full }
      if(tech) { gsub(/^ +| +$/, "", tech); printf ",\"technology\":\"%s\"", tech }
      if(design+0>0 && full+0>0) printf ",\"health_percent\":%.1f", (full+0)/(design+0)*100
      printf "}"
    }'
}

collect_usb_devices() {
  command -v lsusb >/dev/null 2>&1 || return
  lsusb 2>/dev/null | awk '
    /Linux Foundation.*root hub/ { next }
    {
      if (match($0, /ID [0-9a-f]+:[0-9a-f]+/)) {
        id=substr($0, RSTART+3, RLENGTH-3)
        name=substr($0, RSTART+RLENGTH+1)
        gsub(/^ +| +$/, "", name)
      } else next
      if(!name) next
      gsub(/"/, "\\\"", name)
      if(n++) printf ","
      printf "{\"id\":\"%s\",\"name\":\"%s\"}", id, name
    }'
}

# ── Heartbeat: lightweight status sent every 60s ──
send_heartbeat() {
  local payload
  payload=$(cat <<ENDJSON
{
  "agent_version": "$AGENT_VERSION",
  "uptime_seconds": $(collect_uptime_seconds),
  "cpu_percent": "$(collect_cpu_percent)",
  "disk_percent": "$(collect_disk_percent)",
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
ENDJSON
)
  local resp
  resp=$(curl -s --connect-timeout 10 --max-time 30 -o /dev/null -w "%{http_code}" -X POST "${ARTS_SERVER}/api/v1/heartbeat" \
    -H "Content-Type: application/json" \
    -H "X-API-Key: ${ARTS_API_KEY}" \
    -d "$payload" 2>/dev/null)
  echo "Heartbeat: HTTP $resp"
}

# ── Full inventory: collected hourly, sent only if data changed ──
LAST_INVENTORY_HASH=""
CACHE_FILE="/tmp/arts-inventory-cache"

build_inventory_payload() {
  cat <<ENDJSON
{
  "hostname": "$(collect_hostname)",
  "ip_address": "$(collect_ip)",
  "mac_address": "$(collect_mac)",
  "operating_system": "$(collect_os)",
  "processor": "$(collect_processor)",
  "memory_gb": "$(collect_memory_gb)",
  "storage_gb": "$(collect_storage_gb)",
  "serial_number": "$(collect_serial)",
  "make": "$(collect_make)",
  "model": "$(collect_model)",
  "firmware_version": "$(collect_firmware)",
  "bios_vendor": "$(collect_bios_vendor)",
  "bios_date": "$(collect_bios_date)",
  "board_vendor": "$(collect_board_vendor)",
  "board_name": "$(collect_board_name)",
  "board_serial": "$(collect_board_serial)",
  "chassis_type": "$(collect_chassis_type)",
  "network_interfaces": [$(collect_network_interfaces)],
  "software_inventory": [$(collect_software_inventory)],
  "software_usage": [$(collect_software_usage)],
  "gpus": [$(collect_gpus)],
  "ram_slots": [$(collect_ram_slots)],
  "disks": [$(collect_disks)],
  "volumes": [$(collect_volumes)],
  "service_tag": "$(cat /sys/class/dmi/id/product_serial 2>/dev/null | sed 's/["\]/\\&/g' || echo '')",
  "os_architecture": "$(uname -m 2>/dev/null || echo '')",
  "os_version": "$(. /etc/os-release 2>/dev/null && echo "$VERSION_ID" || echo '')",
  "os_build": "$(uname -r 2>/dev/null || echo '')",
  "cpu_cores": "$(collect_cpu_cores)",
  "cpu_threads": "$(collect_cpu_threads)",
  "cpu_max_mhz": "$(collect_cpu_max_mhz)",
  "os_install_date": "$(collect_os_install_date)",
  "last_boot_time": "$(collect_last_boot)",
  "domain": "$(collect_domain)",
  "logged_in_users": [$(collect_logged_in_users)],
  "services": [$(collect_services)],
  "monitors": [$(collect_monitors)],
  "docker_containers": [$(collect_docker_containers)],
  "battery": $(_b=$(collect_battery); echo "${_b:-null}"),
  "usb_devices": [$(collect_usb_devices)],
  "cpu_percent": "$(collect_cpu_percent)",
  "disk_percent": "$(collect_disk_percent)",
  "uptime_seconds": "$(collect_uptime_seconds)",
  "open_ports": [$(collect_open_ports)],
  "dmi_vendor": "$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo '')",
  "dmi_product": "$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo '')",
  "platform_type": "$PLATFORM_TYPE",
  "virt_engine": "$VIRT_ENGINE",
  "agent_version": "$AGENT_VERSION",
  "script_hash": "$(collect_script_hash)",
  "collected_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
ENDJSON
}

send_inventory() {
  local payload
  payload=$(build_inventory_payload)

  # Hash the payload (excluding volatile fields: collected_at, cpu_percent, uptime, disk_percent)
  local stable_hash
  stable_hash=$(echo "$payload" | grep -v '"collected_at"\|"cpu_percent"\|"uptime_seconds"\|"disk_percent"\|"software_usage"' | sha256sum | awk '{print $1}')

  if [ "$stable_hash" = "$LAST_INVENTORY_HASH" ]; then
    echo "Inventory unchanged, skipping full check-in"
    return 0
  fi

  echo "Inventory changed, sending full check-in..."
  local response
  response=$(curl -s --connect-timeout 10 --max-time 60 -w "\n%{http_code}" -X POST "${ARTS_SERVER}/api/v1/checkin" \
    -H "Content-Type: application/json" \
    -H "X-API-Key: ${ARTS_API_KEY}" \
    -d "$payload")

  local http_code body
  http_code=$(echo "$response" | tail -1)
  body=$(echo "$response" | sed '$d')

  if [ "$http_code" -eq 200 ]; then
    LAST_INVENTORY_HASH="$stable_hash"
    echo "Check-in OK"

    # Self-update check
    local update_available
    update_available=$(echo "$body" | grep -o '"available":true' || true)
    if [ -n "$update_available" ]; then
      local update_url update_sha256
      update_url=$(echo "$body" | grep -o '"url":"[^"]*"' | cut -d'"' -f4)
      update_sha256=$(echo "$body" | grep -o '"sha256":"[^"]*"' | cut -d'"' -f4)
      if [ -n "$update_url" ] && [ -n "$update_sha256" ]; then
        echo "Update available, downloading from $update_url"
        local tmpfile
        tmpfile=$(mktemp /tmp/arts-agent-update.XXXXXX)
        if curl -sf --connect-timeout 10 --max-time 30 "$update_url" -o "$tmpfile" 2>/dev/null; then
          local dl_hash
          dl_hash=$(sha256sum "$tmpfile" | awk '{print $1}')
          if [ "$dl_hash" = "$update_sha256" ]; then
            chmod +x "$tmpfile"
            cp "$tmpfile" "$SCRIPT_PATH"
            rm -f "$tmpfile"
            echo "Agent updated, will take effect on next restart"
          else
            echo "Update hash mismatch, skipping" >&2
            rm -f "$tmpfile"
          fi
        else
          rm -f "$tmpfile" 2>/dev/null
        fi
      fi
    fi
  else
    echo "Check-in FAILED (HTTP $http_code): $body" >&2
  fi
}

# ── Main loop: heartbeat every 60s, full inventory every hour ──
HEARTBEAT_INTERVAL=60
INVENTORY_INTERVAL=3600
TICK=0

# Send full inventory on first run
send_inventory
send_heartbeat

echo "Arts Agent $AGENT_VERSION running (heartbeat=${HEARTBEAT_INTERVAL}s, inventory=${INVENTORY_INTERVAL}s)"

while true; do
  sleep "$HEARTBEAT_INTERVAL"
  TICK=$((TICK + HEARTBEAT_INTERVAL))

  send_heartbeat

  if [ "$TICK" -ge "$INVENTORY_INTERVAL" ]; then
    send_inventory
    TICK=0
  fi
done

exit 0
}
