#!/usr/bin/env bash
# AssetTrack network discovery agent — macOS / Linux
# Scans the local subnet via ARP + nmap-lite, reports devices to AssetTrack.
#
# Install:
#   curl -fsSL https://salescollabassettrack.co.za/agent-scripts/unix.sh -o /usr/local/bin/assettrack-agent
#   chmod +x /usr/local/bin/assettrack-agent
#   sudo ASSETTRACK_ENROLL_TOKEN=xxxxx /usr/local/bin/assettrack-agent enroll
#   sudo /usr/local/bin/assettrack-agent run            # foreground (test)
#   # Add to launchd / systemd to run as a service. See docs at /discovery.
#
# Requires: bash, curl, openssl (libressl/openssl), arp, awk. Uses nmap if present.

set -euo pipefail

API_BASE="${ASSETTRACK_API_BASE:-https://salescollabassettrack.co.za}"
STATE_DIR="${ASSETTRACK_STATE_DIR:-/var/lib/assettrack}"
STATE_FILE="$STATE_DIR/agent.json"
AGENT_VERSION="0.1.0"
PLATFORM="$(uname -s)"

mkdir -p "$STATE_DIR"

json_escape() { printf '%s' "$1" | python3 -c 'import sys,json;print(json.dumps(sys.stdin.read()))' 2>/dev/null \
  || node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync(0,"utf8")))'; }

hmac_hex() {
  local key="$1"; local msg="$2"
  printf '%s' "$msg" | openssl dgst -sha256 -hmac "$key" | awk '{print $2}'
}

read_state() {
  [ -r "$STATE_FILE" ] || { echo "No state file. Run: assettrack-agent enroll" >&2; exit 1; }
  AGENT_ID=$(awk -F'"' '/"agentId"/ {print $4}' "$STATE_FILE")
  AGENT_SECRET_HASH=$(awk -F'"' '/"secretHash"/ {print $4}' "$STATE_FILE")
}

enroll() {
  : "${ASSETTRACK_ENROLL_TOKEN:?Set ASSETTRACK_ENROLL_TOKEN to the token from /discovery}"
  local hostname; hostname="$(hostname)"
  local body; body=$(printf '{"enrollmentToken":"%s","hostname":"%s","platform":"%s","agentVersion":"%s"}' \
    "$ASSETTRACK_ENROLL_TOKEN" "$hostname" "$PLATFORM" "$AGENT_VERSION")
  local resp
  resp=$(curl -fsS -X POST "$API_BASE/api/public/agent/enroll" \
    -H "Content-Type: application/json" --data "$body")
  local agentId secret
  agentId=$(printf '%s' "$resp" | awk -F'"' '/"agentId"/ {print $4}')
  secret=$(printf '%s' "$resp" | awk -F'"' '/"agentSecret"/ {print $4}')
  [ -n "$agentId" ] && [ -n "$secret" ] || { echo "Enrollment failed: $resp" >&2; exit 1; }
  # Persist the SHA-256 of the secret (used as the HMAC key for signing).
  local secretHash
  secretHash=$(printf '%s' "$secret" | openssl dgst -sha256 | awk '{print $2}')
  umask 077
  printf '{"agentId":"%s","secretHash":"%s"}\n' "$agentId" "$secretHash" > "$STATE_FILE"
  echo "Enrolled. Agent ID: $agentId"
}

signed_post() {
  local path="$1"; local body="$2"
  local ts; ts=$(date +%s)
  local sig; sig=$(hmac_hex "$AGENT_SECRET_HASH" "${ts}.${body}")
  curl -fsS -X POST "$API_BASE$path" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: $AGENT_ID" \
    -H "X-Agent-Timestamp: $ts" \
    -H "X-Agent-Signature: $sig" \
    --data "$body"
}

scan_devices() {
  # Output JSON array of {mac,ip,hostname,vendor} from arp table.
  local arp_out
  if [ "$PLATFORM" = "Darwin" ]; then
    arp_out=$(arp -an 2>/dev/null || true)
  else
    arp_out=$(ip neigh 2>/dev/null || arp -an 2>/dev/null || true)
  fi
  printf '%s\n' "$arp_out" | awk '
    BEGIN { first=1; printf "[" }
    {
      # Try macOS arp -an: "? (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ..."
      ip=""; mac="";
      for (i=1;i<=NF;i++) {
        if ($i ~ /^\(.*\)$/) { ip=$i; gsub(/[()]/, "", ip); }
        if ($i ~ /^([0-9a-f]{1,2}:){5}[0-9a-f]{1,2}$/) { mac=$i; }
      }
      if (mac == "" || mac == "incomplete" || ip == "") next;
      if (!first) printf ",";
      printf "{\"mac\":\"%s\",\"ip\":\"%s\"}", mac, ip;
      first=0;
    }
    END { printf "]" }
  '
}

cmd_run() {
  read_state
  local devices; devices=$(scan_devices)
  if [ "$devices" = "[]" ]; then
    echo "No devices found in ARP table. Send some traffic on the LAN and retry."
    signed_post "/api/public/agent/heartbeat" "{}" >/dev/null
    return 0
  fi
  local body; body=$(printf '{"devices":%s}' "$devices")
  signed_post "/api/public/agent/devices" "$body"
  echo
}

cmd_loop() {
  read_state
  while true; do
    cmd_run || echo "scan failed; will retry"
    sleep "${ASSETTRACK_INTERVAL:-300}"
  done
}

case "${1:-}" in
  enroll) enroll ;;
  run)    cmd_run ;;
  loop)   cmd_loop ;;
  *) echo "Usage: $0 {enroll|run|loop}" >&2; exit 2 ;;
esac
