#!/usr/bin/env bash
#
# ArchNest Docker monitoring agent (self-hosted, push model).
#
# Collects a rich snapshot of this host's Docker containers (docker ps +
# docker inspect + a docker stats snapshot) and POSTs it to ArchNest. ArchNest
# stores the latest report per host and shows it read-only on the Containers
# page. This is MONITORING ONLY — it never receives or runs commands.
#
# Requirements: bash, docker, curl, jq.
#
# Configuration (env vars; may live in /etc/archnest/agent.env):
#   ARCHNEST_URL          Base URL of the ArchNest backend, reachable over your
#                         mesh / private network, e.g. http://100.64.0.5:4000
#   ARCHNEST_AGENT_TOKEN  Shared token; must match the backend's ARCHNEST_AGENT_TOKEN.
#   ARCHNEST_HOST_ID      Stable id for this host, e.g. "proxmox-vm-1"
#                         (allowed: letters, digits, . _ - ; max 128 chars).
#   ARCHNEST_HOSTNAME     Optional display hostname (defaults to `hostname`).
#
# Exit codes: 0 ok, 1 misconfig/missing deps, 2 report POST failed.

set -euo pipefail

AGENT_VERSION="1"

# Load config file if present (does not override already-exported env).
if [ -f /etc/archnest/agent.env ]; then
  # shellcheck disable=SC1091
  . /etc/archnest/agent.env
fi

err() { echo "archnest-docker-agent: $*" >&2; }

# --- Dependency + config checks -------------------------------------------
for bin in docker curl jq; do
  if ! command -v "$bin" >/dev/null 2>&1; then
    err "missing required dependency: $bin"
    exit 1
  fi
done

: "${ARCHNEST_URL:?ARCHNEST_URL is required}"
: "${ARCHNEST_AGENT_TOKEN:?ARCHNEST_AGENT_TOKEN is required}"
: "${ARCHNEST_HOST_ID:?ARCHNEST_HOST_ID is required}"
HOSTNAME_VALUE="${ARCHNEST_HOSTNAME:-$(hostname)}"

if ! printf '%s' "$ARCHNEST_HOST_ID" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'; then
  err "ARCHNEST_HOST_ID '$ARCHNEST_HOST_ID' is invalid (allowed: A-Z a-z 0-9 . _ - , max 128)"
  exit 1
fi

REPORT_URL="${ARCHNEST_URL%/}/api/agents/docker/report"

# --- Collect container ids -------------------------------------------------
mapfile -t IDS < <(docker ps --all --no-trunc --format '{{.ID}}')

# --- Stats snapshot (one shot) keyed by full id ----------------------------
# `docker stats` reports a short id; we map short->full via the ids list.
# Build a jq object: { "<shortid>": {cpu,mem,...} }.
STATS_JSON="$(docker stats --no-stream --no-trunc \
  --format '{{.ID}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null \
  | jq -R -s '
      def bytes:
        # converts "12.3MiB" / "1.2GB" etc to a number of bytes
        capture("(?<n>[0-9.]+)\\s*(?<u>[A-Za-z]*)") as $m
        | ($m.n | tonumber) as $n
        | ($m.u | ascii_downcase) as $u
        | $n * (
            if   $u|startswith("ki") then 1024
            elif $u|startswith("mi") then 1048576
            elif $u|startswith("gi") then 1073741824
            elif $u|startswith("ti") then 1099511627776
            elif $u|startswith("kb") or $u=="k" then 1000
            elif $u|startswith("mb") or $u=="m" then 1000000
            elif $u|startswith("gb") or $u=="g" then 1000000000
            elif $u|startswith("tb") or $u=="t" then 1000000000000
            elif $u|startswith("b")  or $u=="" then 1
            else 1 end
          ) | floor;
      split("\n") | map(select(length > 0)) | map(split("|")) | map({
        key: .[0],
        value: {
          cpuPercent: (.[1] | gsub("%";"") | tonumber? // 0),
          memUsage:   (.[2] | split("/")[0] | gsub(" ";"") | (try bytes catch 0)),
          memLimit:   (.[2] | split("/")[1] | gsub(" ";"") | (try bytes catch 0)),
          netRxBytes: (.[3] | split("/")[0] | gsub(" ";"") | (try bytes catch 0)),
          netTxBytes: (.[3] | split("/")[1] | gsub(" ";"") | (try bytes catch 0)),
          blockReadBytes:  (.[4] | split("/")[0] | gsub(" ";"") | (try bytes catch 0)),
          blockWriteBytes: (.[4] | split("/")[1] | gsub(" ";"") | (try bytes catch 0))
        }
      }) | from_entries
    ')"
[ -z "$STATS_JSON" ] && STATS_JSON='{}'

# --- Per-container detail from docker inspect ------------------------------
# jq transform turning one inspect object into our report schema, masking
# secret-looking env values.
INSPECT_FILTER='
  def mask($k): ($k | ascii_upcase) as $u
    | ($u | test("PASS|SECRET|TOKEN|KEY|PRIVATE|CREDENTIAL"));
  .[0] as $c
  | {
      id: $c.Id,
      name: ($c.Name // "" | ltrimstr("/")),
      image: ($c.Config.Image // ""),
      imageId: ($c.Image // ""),
      state: ($c.State.Status // "unknown"),
      status: ($c.State.Status // ""),
      createdAt: ($c.Created // null),
      startedAt: ($c.State.StartedAt // null),
      restartCount: ($c.RestartCount // 0),
      restartPolicy: ($c.HostConfig.RestartPolicy.Name // ""),
      health: ($c.State.Health.Status // "none"),
      ports: (
        ($c.NetworkSettings.Ports // {}) | to_entries | map(
          (.key | split("/")) as $p
          | (.value // [])[]? as $b
          | { hostIp: ($b.HostIp // ""), hostPort: ($b.HostPort | tonumber? // null),
              containerPort: ($p[0] | tonumber? // 0), proto: ($p[1] // "tcp") }
        )
      ),
      networks: (
        ($c.NetworkSettings.Networks // {}) | to_entries
        | map({ name: .key, ip: (.value.IPAddress // "") })
      ),
      mounts: (
        ($c.Mounts // []) | map({
          type: (.Type // ""), source: (.Source // .Name // ""),
          destination: (.Destination // ""), rw: (.RW // true)
        })
      ),
      env: (
        ($c.Config.Env // []) | map(
          (. | split("=")) as $kv
          | { key: $kv[0], value: (if mask($kv[0]) then "********" else ($kv[1:] | join("=")) end) }
        )
      ),
      command: (($c.Config.Entrypoint // []) + ($c.Config.Cmd // []) | join(" ")),
      labels: ($c.Config.Labels // {})
    }
'

CONTAINERS='[]'
for id in "${IDS[@]}"; do
  [ -z "$id" ] && continue
  detail="$(docker inspect "$id" 2>/dev/null | jq -c "$INSPECT_FILTER" 2>/dev/null || true)"
  [ -z "$detail" ] && continue
  short="${id:0:12}"
  # Attach the matching stats snapshot (match by full or short id).
  detail="$(jq -c --argjson stats "$STATS_JSON" --arg id "$id" --arg short "$short" \
    '. + { stats: ($stats[$id] // $stats[$short] // null) }' <<<"$detail")"
  CONTAINERS="$(jq -c --argjson c "$detail" '. + [$c]' <<<"$CONTAINERS")"
done

# --- Assemble + POST -------------------------------------------------------
PAYLOAD="$(jq -n \
  --arg hostId "$ARCHNEST_HOST_ID" \
  --arg hostname "$HOSTNAME_VALUE" \
  --arg agentVersion "$AGENT_VERSION" \
  --arg reportedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --argjson containers "$CONTAINERS" \
  '{ hostId: $hostId, hostname: $hostname, agentVersion: $agentVersion, reportedAt: $reportedAt, containers: $containers }')"

HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' \
  -X POST "$REPORT_URL" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${ARCHNEST_AGENT_TOKEN}" \
  --data-binary "$PAYLOAD" || echo "000")"

if [ "$HTTP_CODE" != "200" ]; then
  err "report POST to $REPORT_URL failed (HTTP $HTTP_CODE)"
  exit 2
fi

echo "archnest-docker-agent: reported ${#IDS[@]} container(s) as '$ARCHNEST_HOST_ID' (HTTP $HTTP_CODE)"
