#!/bin/bash

###############################################################################
# Pinguzo Server Monitoring Agent
# Collects system metrics and sends to Pinguzo edge servers
###############################################################################

# ── Agent Version ─────────────────────────────────────────────────────────────
AGENT_VERSION="1.1.0"

# ── Load configuration ────────────────────────────────────────────────────────
if [ -f /usr/local/pinguzo/config/agent.conf ]; then
    source /usr/local/pinguzo/config/agent.conf
fi

# ── Configuration ─────────────────────────────────────────────────────────────
# PINGUZO_API_URL holds the base URL only (e.g. https://in-mum.pinguzo.com).
# Endpoint paths are appended at call time so other endpoints can be used in future.
API_BASE="${PINGUZO_API_URL:-}"
METRICS_ENDPOINT="/api/v1/metrics.php"
AGENT_KEY="${PINGUZO_AGENT_KEY:-}"
QUEUE_DIR="${PINGUZO_QUEUE_DIR:-/usr/local/pinguzo/queue}"
QUEUE_FILE="$QUEUE_DIR/queue.dat"
LOCK_FILE="/var/run/pinguzo-agent.lock"
LOG_FILE="${PINGUZO_LOG_FILE:-/var/log/pinguzo/agent.log}"
DAILY_STAMP_FILE="$QUEUE_DIR/daily.stamp"   # tracks last daily-info send
UPDATE_STAMP_FILE="$QUEUE_DIR/update.stamp" # tracks last update check
PINGUZO_DIR="/usr/local/pinguzo"
MAX_QUEUE_SIZE=1000
TOP_PROCESSES=100  # Number of top processes to track (full list like old agent)

# Update check URL — called once per day with version + OS data
UPDATE_API_URL="https://api.pinguzo.com/updates.php"

# Error handling
set -o pipefail

# ── Logging ───────────────────────────────────────────────────────────────────
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" 2>&1
}

# ── Validate required config ──────────────────────────────────────────────────
if [ -z "$AGENT_KEY" ]; then
    log "ERROR: AGENT_KEY not set"
    exit 1
fi

# ── Ensure directories exist ──────────────────────────────────────────────────
mkdir -p "$QUEUE_DIR" 2>/dev/null
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null

# ── Prevent multiple instances ────────────────────────────────────────────────
LOCK_MAX_AGE=300  # 5 minutes — kill stuck processes older than this
if [ -f "$LOCK_FILE" ]; then
    OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null)
    if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
        # Process is still running — check how long it has been running
        PROC_START=$(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0)
        NOW=$(date +%s)
        AGE=$(( NOW - PROC_START ))
        if [ "$AGE" -lt "$LOCK_MAX_AGE" ]; then
            # Still within time limit — let it finish
            exit 0
        fi
        # Stuck for too long — kill it and take over
        log "WARN: Killing stuck agent process $OLD_PID (running ${AGE}s)"
        kill -9 "$OLD_PID" 2>/dev/null
        sleep 1
    fi
    rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
trap "rm -f $LOCK_FILE" EXIT

# ═════════════════════════════════════════════════════════════════════════════
# METRIC COLLECTORS
# ═════════════════════════════════════════════════════════════════════════════

# Collect CPU usage (%) — overall, 1-second sample
get_cpu_usage() {
    local cpu=""
    # /proc/stat is the most reliable cross-distro method
    if [ -f /proc/stat ]; then
        local line1=$(grep '^cpu ' /proc/stat)
        sleep 1
        local line2=$(grep '^cpu ' /proc/stat)
        local idle1=$(echo "$line1" | awk '{print $5}')
        local total1=$(echo "$line1" | awk '{s=0; for(i=2;i<=NF;i++) s+=$i; print s}')
        local idle2=$(echo "$line2" | awk '{print $5}')
        local total2=$(echo "$line2" | awk '{s=0; for(i=2;i<=NF;i++) s+=$i; print s}')
        local diff_idle=$((idle2 - idle1))
        local diff_total=$((total2 - total1))
        if [ "$diff_total" -gt 0 ]; then
            cpu=$(awk "BEGIN {printf \"%.2f\", (1 - $diff_idle/$diff_total) * 100}")
        fi
    fi
    # Fallback: mpstat
    if [ -z "$cpu" ] && command -v mpstat &>/dev/null; then
        cpu=$(mpstat 1 1 | awk '/Average/ {printf "%.2f", 100 - $NF}')
    fi
    echo "${cpu:-0}"
}

# Collect CPU static info: model, cores, speed
get_cpu_info() {
    local cpu_model=""
    local cpu_cores=0
    local cpu_speed=""

    if [ -f /proc/cpuinfo ]; then
        cpu_model=$(grep 'model name' /proc/cpuinfo | head -1 | awk -F': ' '{print $2}' | sed 's/^[[:space:]]*//' | sed 's/[\\\"]/\\&/g' | tr -d '\n')
        cpu_cores=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo 0)
        cpu_speed=$(grep 'cpu MHz' /proc/cpuinfo | head -1 | awk -F': ' '{print $2}' | sed 's/^[[:space:]]*//' | tr -d '\n')
    fi

    # Fallback for cpu_speed via lscpu
    if [ -z "$cpu_speed" ] && command -v lscpu &>/dev/null; then
        cpu_speed=$(lscpu 2>/dev/null | grep 'CPU MHz' | awk -F': ' '{print $2}' | tr -d ' ')
        if [ -z "$cpu_speed" ]; then
            cpu_speed=$(lscpu 2>/dev/null | grep 'CPU max MHz' | awk -F': ' '{print $2}' | tr -d ' ')
        fi
    fi

    # Sanitise
    [[ "$cpu_cores" =~ ^[0-9]+$ ]] || cpu_cores=0
    [[ "$cpu_speed"  =~ ^[0-9]+(\.[0-9]+)?$ ]] || cpu_speed="0"
    cpu_model="${cpu_model:-Unknown}"

    echo "${cpu_model}|${cpu_cores}|${cpu_speed}"
}

# Collect memory + swap usage
get_memory_stats() {
    if command -v free &>/dev/null; then
        free -m | awk '
            NR==2 { mem_pct=($2>0)?($3/$2)*100:0; mem_used=$3; mem_total=$2 }
            NR==3 { swap_pct=($2>0)?($3/$2)*100:0; swap_used=$3; swap_total=$2 }
            END   { printf "%.2f|%d|%d|%.2f|%d|%d", mem_pct, mem_used, mem_total, swap_pct, swap_used, swap_total }
        '
    elif [ -f /proc/meminfo ]; then
        # Fallback: parse /proc/meminfo directly (values in kB)
        awk '
            /^MemTotal:/    { mem_total=$2 }
            /^MemFree:/     { mem_free=$2 }
            /^Buffers:/     { buffers=$2 }
            /^Cached:/      { cached=$2 }
            /^SwapTotal:/   { swap_total=$2 }
            /^SwapFree:/    { swap_free=$2 }
            END {
                mem_used = mem_total - mem_free - buffers - cached
                if (mem_used < 0) mem_used = 0
                mem_pct = (mem_total > 0) ? (mem_used / mem_total) * 100 : 0
                swap_used = swap_total - swap_free
                swap_pct = (swap_total > 0) ? (swap_used / swap_total) * 100 : 0
                printf "%.2f|%d|%d|%.2f|%d|%d",
                    mem_pct, int(mem_used/1024), int(mem_total/1024),
                    swap_pct, int(swap_used/1024), int(swap_total/1024)
            }
        ' /proc/meminfo
    else
        echo "0|0|0|0|0|0"
    fi
}

# Collect disk usage for ALL mounted partitions (base64-encoded JSON array)
# Returns a base64 string containing a JSON array of partition objects
get_all_disks() {
    local json="["
    local first=true

    # Get all real partitions (simfs or real block devices)
    while IFS= read -r line; do
        # df -P -T output: Filesystem Type 1K-blocks Used Available Use% Mounted
        local fs=$(echo "$line" | awk '{print $1}')
        local fstype=$(echo "$line" | awk '{print $2}')
        local size_kb=$(echo "$line" | awk '{print $3}')
        local used_kb=$(echo "$line" | awk '{print $4}')
        local avail_kb=$(echo "$line" | awk '{print $5}')
        local pct=$(echo "$line" | awk '{gsub(/%/,"",$6); print $6}')
        local mount=$(echo "$line" | awk '{print $7}')

        # Sanitise numeric fields
        [[ "$size_kb"  =~ ^[0-9]+$ ]] || size_kb=0
        [[ "$used_kb"  =~ ^[0-9]+$ ]] || used_kb=0
        [[ "$avail_kb" =~ ^[0-9]+$ ]] || avail_kb=0
        [[ "$pct"      =~ ^[0-9]+$ ]] || pct=0

        # Escape strings for JSON
        fs=$(echo "$fs"    | sed 's/[\\\"]/\\&/g')
        fstype=$(echo "$fstype" | sed 's/[\\\"]/\\&/g')
        mount=$(echo "$mount"  | sed 's/[\\\"]/\\&/g')

        [ "$first" = false ] && json+=","
        first=false
        json+="{\"fs\":\"$fs\",\"type\":\"$fstype\",\"size_kb\":$size_kb,\"used_kb\":$used_kb,\"avail_kb\":$avail_kb,\"pct\":$pct,\"mount\":\"$mount\"}"
    done < <(df -P -T -B 1k 2>/dev/null | grep -E '^simfs|^/' | tail -n +1)

    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# Collect disk inodes for ALL mounted partitions (base64-encoded JSON array)
get_all_inodes() {
    local json="["
    local first=true

    while IFS= read -r line; do
        local fs=$(echo "$line" | awk '{print $1}')
        local inodes=$(echo "$line" | awk '{print $2}')
        local iused=$(echo "$line" | awk '{print $3}')
        local ifree=$(echo "$line" | awk '{print $4}')
        local ipct=$(echo "$line" | awk '{gsub(/%/,"",$5); print $5}')
        local mount=$(echo "$line" | awk '{print $6}')

        [[ "$inodes" =~ ^[0-9]+$ ]] || inodes=0
        [[ "$iused"  =~ ^[0-9]+$ ]] || iused=0
        [[ "$ifree"  =~ ^[0-9]+$ ]] || ifree=0
        [[ "$ipct"   =~ ^[0-9]+$ ]] || ipct=0

        fs=$(echo "$fs"    | sed 's/[\\\"]/\\&/g')
        mount=$(echo "$mount"  | sed 's/[\\\"]/\\&/g')

        [ "$first" = false ] && json+=","
        first=false
        json+="{\"fs\":\"$fs\",\"inodes\":$inodes,\"iused\":$iused,\"ifree\":$ifree,\"pct\":$ipct,\"mount\":\"$mount\"}"
    done < <(df -P -i 2>/dev/null | grep -E '^simfs|^/' | tail -n +1)

    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# Collect disk usage for root partition + disk I/O (used for VictoriaMetrics scalars)
get_disk_stats() {
    local disk_pct=$(df / 2>/dev/null | awk 'NR==2 {gsub(/%/,""); print $5}')

    local dev=""
    for candidate in sda vda xvda nvme0n1 hda; do
        if grep -q " $candidate " /proc/diskstats 2>/dev/null; then
            dev="$candidate"; break
        fi
    done

    local disk_read_bytes=0
    local disk_write_bytes=0
    if [ -n "$dev" ]; then
        local stats=$(grep " $dev " /proc/diskstats 2>/dev/null | awk '{print $6, $10}')
        local read_sectors=$(echo "$stats" | awk '{print $1}')
        local write_sectors=$(echo "$stats" | awk '{print $2}')
        disk_read_bytes=$(( ${read_sectors:-0} * 512 ))
        disk_write_bytes=$(( ${write_sectors:-0} * 512 ))
    fi

    echo "${disk_pct:-0}|${disk_read_bytes}|${disk_write_bytes}"
}

# Collect load averages (1m, 5m, 15m)
get_load_averages() {
    if [ -f /proc/loadavg ]; then
        awk '{print $1"|"$2"|"$3}' /proc/loadavg
    else
        uptime | awk -F'load average:' '{
            split($2, a, ",")
            gsub(/ /,"",a[1]); gsub(/ /,"",a[2]); gsub(/ /,"",a[3])
            print a[1]"|"a[2]"|"a[3]
        }'
    fi
}

# Collect system uptime in seconds
get_uptime_seconds() {
    if [ -f /proc/uptime ]; then
        awk '{printf "%d", $1}' /proc/uptime
    else
        echo "0"
    fi
}

# Collect open file descriptors
get_open_files() {
    if [ -f /proc/sys/fs/file-nr ]; then
        awk '{print $1}' /proc/sys/fs/file-nr
    else
        echo "0"
    fi
}

# Collect TCP connection counts
get_tcp_connections() {
    local established=0
    local time_wait=0
    local close_wait=0
    if [ -f /proc/net/tcp ] || [ -f /proc/net/tcp6 ]; then
        established=$(cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk '$4=="01"' | wc -l)
        time_wait=$(cat    /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk '$4=="06"' | wc -l)
        close_wait=$(cat   /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk '$4=="08"' | wc -l)
    elif command -v ss &>/dev/null; then
        established=$(ss -t state established 2>/dev/null | tail -n +2 | wc -l)
        time_wait=$(ss   -t state time-wait   2>/dev/null | tail -n +2 | wc -l)
        close_wait=$(ss  -t state close-wait  2>/dev/null | tail -n +2 | wc -l)
    fi
    echo "${established}|${time_wait}|${close_wait}"
}

# Collect SSH sessions (logged-in users)
get_ssh_sessions() {
    who 2>/dev/null | wc -l || echo "0"
}

# Collect SSH session details (base64-encoded JSON array)
# Each entry: {user, tty, since, from}
get_ssh_session_details() {
    local json="["
    local first=true
    while IFS= read -r line; do
        [ -z "$line" ] && continue
        local user tty since from
        user=$(echo "$line" | awk '{print $1}')
        tty=$(echo "$line"  | awk '{print $2}')
        since=$(echo "$line" | awk '{print $3, $4}')
        from=$(echo "$line"  | awk '{print $5}' | tr -d '()')
        user=$(echo "$user"  | sed 's/[\\\"]/\\&/g')
        tty=$(echo "$tty"    | sed 's/[\\\"]/\\&/g')
        since=$(echo "$since"| sed 's/[\\\"]/\\&/g')
        from=$(echo "$from"  | sed 's/[\\\"]/\\&/g')
        [ "$first" = false ] && json+=","
        first=false
        json+="{\"user\":\"$user\",\"tty\":\"$tty\",\"since\":\"$since\",\"from\":\"$from\"}"
    done < <(who 2>/dev/null)
    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# Collect ALL network interfaces stats (base64-encoded JSON array)
# Mirrors old agent's "interfaces" field: all interfaces with rx/tx bytes/packets/errors
get_all_interfaces() {
    local json="["
    local first=true

    # Read /proc/net/dev (skip header lines)
    while IFS= read -r line; do
        # Format: iface: rx_bytes rx_pkts rx_errs ... tx_bytes tx_pkts tx_errs ...
        local iface=$(echo "$line" | awk -F: '{print $1}' | tr -d ' ')
        [ -z "$iface" ] || [ "$iface" = "lo" ] && continue

        local rx_bytes=$(echo "$line" | awk '{print $2}')
        local rx_pkts=$(echo "$line"  | awk '{print $3}')
        local rx_errs=$(echo "$line"  | awk '{print $4}')
        local tx_bytes=$(echo "$line" | awk '{print $10}')
        local tx_pkts=$(echo "$line"  | awk '{print $11}')
        local tx_errs=$(echo "$line"  | awk '{print $12}')

        [[ "$rx_bytes" =~ ^[0-9]+$ ]] || rx_bytes=0
        [[ "$rx_pkts"  =~ ^[0-9]+$ ]] || rx_pkts=0
        [[ "$rx_errs"  =~ ^[0-9]+$ ]] || rx_errs=0
        [[ "$tx_bytes" =~ ^[0-9]+$ ]] || tx_bytes=0
        [[ "$tx_pkts"  =~ ^[0-9]+$ ]] || tx_pkts=0
        [[ "$tx_errs"  =~ ^[0-9]+$ ]] || tx_errs=0

        iface=$(echo "$iface" | sed 's/[\\\"]/\\&/g')

        [ "$first" = false ] && json+=","
        first=false
        json+="{\"iface\":\"$iface\",\"rx_bytes\":$rx_bytes,\"rx_pkts\":$rx_pkts,\"rx_errs\":$rx_errs,\"tx_bytes\":$tx_bytes,\"tx_pkts\":$tx_pkts,\"tx_errs\":$tx_errs}"
    done < <(tail -n +3 /proc/net/dev 2>/dev/null)

    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# Collect primary interface network stats (for VictoriaMetrics scalars)
get_network_stats() {
    local interface=""
    if [ -f /sys/class/net/eth0/statistics/rx_bytes ]; then
        interface="eth0"
    elif [ -f /sys/class/net/ens3/statistics/rx_bytes ]; then
        interface="ens3"
    elif [ -f /sys/class/net/enp0s3/statistics/rx_bytes ]; then
        interface="enp0s3"
    else
        interface=$(/usr/sbin/ip route get 4.2.2.1 2>/dev/null | grep dev | awk -F'dev' '{print $2}' | awk '{print $1}')
        [ -z "$interface" ] && interface=$(ls /sys/class/net 2>/dev/null | grep -v lo | head -1)
    fi

    if [ -n "$interface" ]; then
        local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes   2>/dev/null || echo 0)
        local tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes   2>/dev/null || echo 0)
        local rx_pkts=$(cat  /sys/class/net/$interface/statistics/rx_packets 2>/dev/null || echo 0)
        local tx_pkts=$(cat  /sys/class/net/$interface/statistics/tx_packets 2>/dev/null || echo 0)
        local rx_errs=$(cat  /sys/class/net/$interface/statistics/rx_errors  2>/dev/null || echo 0)
        local tx_errs=$(cat  /sys/class/net/$interface/statistics/tx_errors  2>/dev/null || echo 0)
        echo "$interface|$rx_bytes|$tx_bytes|$rx_pkts|$tx_pkts|$rx_errs|$tx_errs"
    else
        echo "eth0|0|0|0|0|0|0"
    fi
}

# Collect all IPv4 addresses (base64-encoded JSON array)
get_ipv4_addresses() {
    local json="["
    local first=true

    # Use "/usr/sbin/ip -o addr show" (one line per address, iface at $2).
    # Avoid "-f inet" — unsupported on some distros (e.g. cPanel/CloudLinux).
    # Filter for " inet " lines to get IPv4 only.
    while IFS= read -r line; do
        echo "$line" | grep -q ' inet ' || continue
        local iface ip
        iface=$(echo "$line" | awk '{print $2}')
        ip=$(echo "$line"    | awk '{for(i=1;i<=NF;i++){if($i=="inet"){split($(i+1),a,"/");print a[1];exit}}}')
        [ -z "$iface" ] || [ -z "$ip" ] && continue
        iface=$(echo "$iface" | sed 's/[\\\"]/\\&/g')
        ip=$(echo "$ip"       | sed 's/[\\\"]/\\&/g')
        [ "$first" = false ] && json+=","
        first=false
        json+="{\"iface\":\"$iface\",\"ip\":\"$ip\"}"
    done < <(/usr/sbin/ip -o addr show 2>/dev/null)

    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# Collect all IPv6 addresses (base64-encoded JSON array)
get_ipv6_addresses() {
    local json="["
    local first=true

    while IFS= read -r line; do
        echo "$line" | grep -q ' inet6 ' || continue
        local iface ip
        iface=$(echo "$line" | awk '{print $2}')
        ip=$(echo "$line"    | awk '{for(i=1;i<=NF;i++){if($i=="inet6"){split($(i+1),a,"/");print a[1];exit}}}')
        [ -z "$iface" ] || [ -z "$ip" ] && continue
        # Skip link-local addresses
        echo "$ip" | grep -q '^fe80' && continue
        iface=$(echo "$iface" | sed 's/[\\\"]/\\&/g')
        ip=$(echo "$ip"       | sed 's/[\\\"]/\\&/g')
        [ "$first" = false ] && json+=","
        first=false
        json+="{\"iface\":\"$iface\",\"ip\":\"$ip\"}"
    done < <(/usr/sbin/ip -o addr show 2>/dev/null)

    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# Collect top processes by CPU — sends all with ps -e (like old agent) up to TOP_PROCESSES
# Returns a base64-encoded JSON array so large process lists don't break the JSON payload
get_top_processes() {
    local json="["
    local first=true
    local count=0

    # Probe which ps column names work on this system.
    # GNU procps uses "pcpu"/"pmem"; some distros (cPanel, CloudLinux) use "%cpu"/"%mem".
    # Test BOTH together — pcpu can be valid while pmem is not, causing silent empty output.
    # --sort may also be unsupported — fall back to post-sort with sort(1).
    local cpu_col="%cpu" mem_col="%mem"
    ps -eo pcpu,pmem &>/dev/null && { cpu_col="pcpu"; mem_col="pmem"; }

    # Build sorted process list; tolerate ps versions that don't support --sort
    local ps_out
    ps_out=$(ps -eo pid,ppid,rss,vsz,user,${mem_col},${cpu_col},comm,cmd \
             --sort=-${cpu_col},-${mem_col} 2>/dev/null | tail -n +2)
    if [ -z "$ps_out" ]; then
        # --sort not supported; collect unsorted then sort numerically on col 7 (cpu%)
        ps_out=$(ps -eo pid,ppid,rss,vsz,user,${mem_col},${cpu_col},comm,cmd \
                 2>/dev/null | tail -n +2 | sort -k7 -rn)
    fi

    while IFS= read -r line; do
        [ -z "$line" ] && continue
        local pid ppid rss vsz user pmem pcpu comm cmd
        pid=$(echo "$line"  | awk '{print $1}')
        ppid=$(echo "$line" | awk '{print $2}')
        rss=$(echo "$line"  | awk '{print $3}')
        vsz=$(echo "$line"  | awk '{print $4}')
        user=$(echo "$line" | awk '{print $5}')
        pmem=$(echo "$line" | awk '{print $6}')
        pcpu=$(echo "$line" | awk '{print $7}')
        comm=$(echo "$line" | awk '{print $8}')
        cmd=$(echo "$line"  | awk '{for(i=9;i<=NF;i++) printf "%s ", $i}' | \
                    sed 's/ $//' | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed 's/[[:cntrl:]]//g')

        # Sanitise numerics
        [[ "$pid"  =~ ^[0-9]+$ ]]            || continue
        [[ "$ppid" =~ ^[0-9]+$ ]]            || ppid=0
        [[ "$rss"  =~ ^[0-9]+$ ]]            || rss=0
        [[ "$vsz"  =~ ^[0-9]+$ ]]            || vsz=0
        [[ "$pcpu" =~ ^[0-9]+(\.[0-9]+)?$ ]] || pcpu="0"
        [[ "$pmem" =~ ^[0-9]+(\.[0-9]+)?$ ]] || pmem="0"
        user=$(echo "$user" | sed 's/[\\\"]/\\&/g' | tr -d '[:cntrl:]')
        comm=$(echo "$comm" | sed 's/[\\\"]/\\&/g' | tr -d '[:cntrl:]')

        [ "$first" = false ] && json+=","
        first=false
        json+="{\"pid\":$pid,\"ppid\":$ppid,\"rss\":$rss,\"vsz\":$vsz,\"user\":\"$user\",\"cpu\":$pcpu,\"mem\":$pmem,\"comm\":\"$comm\",\"cmd\":\"$cmd\"}"

        count=$((count + 1))
        [ $count -ge $TOP_PROCESSES ] && break
    done <<< "$ps_out"

    json+="]"
    echo "$json" | base64 | tr -d '\n'
}

# ═════════════════════════════════════════════════════════════════════════════
# DAILY-ONLY COLLECTORS (OS info + SMART health + CPU static info)
# Run at most once per 24 hours to avoid wasting bandwidth every minute.
# ═════════════════════════════════════════════════════════════════════════════

# Collect OS / distro information
get_os_info() {
    local os_name=""
    local os_version=""
    local os_arch=""
    local kernel=""

    # Pretty name from /etc/os-release (most distros)
    if [ -f /etc/os-release ]; then
        os_name=$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-$NAME}")
        os_version=$(. /etc/os-release 2>/dev/null && echo "${VERSION_ID:-}")
    fi

    # Fallback: lsb_release
    if [ -z "$os_name" ] && command -v lsb_release &>/dev/null; then
        os_name=$(lsb_release -ds 2>/dev/null | tr -d '"')
        os_version=$(lsb_release -rs 2>/dev/null)
    fi

    # Fallback: /etc/redhat-release
    if [ -z "$os_name" ] && [ -f /etc/redhat-release ]; then
        os_name=$(cat /etc/redhat-release)
    fi

    os_arch=$(uname -m 2>/dev/null || echo "unknown")
    kernel=$(uname -r 2>/dev/null || echo "unknown")

    # Sanitise: remove special chars unsafe for JSON
    os_name=$(echo "${os_name:-Linux}" | sed 's/[\\\"]/\\&/g' | tr -d '\n')
    os_version=$(echo "${os_version:-}" | sed 's/[\\\"]/\\&/g' | tr -d '\n')

    echo "{\"os\":\"$os_name\",\"os_version\":\"$os_version\",\"arch\":\"$os_arch\",\"kernel\":\"$kernel\"}"
}

# Collect SMART disk health for all physical drives
# Returns JSON: {"drives":[{"dev":"sda","smart":"passed"},{"dev":"sdb","smart":"failed"}]}
get_smart_health() {
    local drives_json="["
    local first=true

    # Find all block devices (no partitions: exclude sda1 etc.)
    local devs=""
    if command -v lsblk &>/dev/null; then
        devs=$(lsblk -dno NAME,TYPE 2>/dev/null | awk '$2=="disk"{print $1}')
    else
        devs=$(ls /sys/block 2>/dev/null | grep -Ev '^(loop|ram|md|dm-)' || true)
    fi

    for dev in $devs; do
        local dev_path="/dev/$dev"
        [ -b "$dev_path" ] || continue

        local status="unknown"
        local temp="null"
        local reallocated="null"

        # Locate smartctl — check common sbin paths in case PATH is stripped (cron/systemd)
        local smartctl_bin=""
        for _bin in smartctl /usr/sbin/smartctl /sbin/smartctl /usr/local/sbin/smartctl; do
            if command -v "$_bin" &>/dev/null 2>&1 || [ -x "$_bin" ]; then
                smartctl_bin="$_bin"
                break
            fi
        done

        if [ -n "$smartctl_bin" ]; then
            # -H: health check  -A: attributes
            # Capture stdout+stderr together (smartctl writes errors to stderr on VMs/virtual disks)
            # Always try sudo — non-root runs often fail silently on physical drives too
            local smart_out
            smart_out=$(sudo "$smartctl_bin" -H -A "$dev_path" 2>&1) || true
            # Fallback: try without sudo (some setups use setuid or capabilities)
            if [ -z "$smart_out" ]; then
                smart_out=$("$smartctl_bin" -H -A "$dev_path" 2>&1) || true
            fi

            if echo "$smart_out" | grep -qi "SMART overall-health.*PASSED\|result: PASSED\|test result: PASSED"; then
                status="passed"
            elif echo "$smart_out" | grep -qi "FAILED\|FAILING"; then
                status="failed"
            elif echo "$smart_out" | grep -qi "NOT SUPPORTED\|Unable to detect\|No such device\|Operation not supported\|open device.*failed"; then
                status="unsupported"
            fi

            # Drive temperature (attribute 194 or 190)
            temp=$(echo "$smart_out" | awk '$1=="194"||$1=="190" {print $10; exit}')
            temp=$(echo "$temp" | tr -d '[:space:]')
            [[ "$temp" =~ ^[0-9]+$ ]] || temp="null"

            # Reallocated sectors (attribute 5) — non-zero = disk is degrading
            reallocated=$(echo "$smart_out" | awk '$1=="5" {print $10; exit}')
            reallocated=$(echo "$reallocated" | tr -d '[:space:]')
            [[ "$reallocated" =~ ^[0-9]+$ ]] || reallocated="null"
        fi

        local dev_escaped=$(echo "$dev" | sed 's/[\\\"]/\\&/g')
        [ "$first" = false ] && drives_json+=","
        first=false
        drives_json+="{\"dev\":\"$dev_escaped\",\"smart\":\"$status\""
        [ "$temp" != "null" ]        && drives_json+=",\"temp_c\":$temp"
        [ "$reallocated" != "null" ] && drives_json+=",\"reallocated_sectors\":$reallocated"
        drives_json+="}"
    done

    drives_json+="]"
    echo "{\"drives\":$drives_json}"
}

# ── Check whether we should send daily info today ─────────────────────────────
should_send_daily() {
    local today=$(date +%Y-%m-%d)
    if [ ! -f "$DAILY_STAMP_FILE" ] || [ "$(cat "$DAILY_STAMP_FILE" 2>/dev/null)" != "$today" ]; then
        return 0
    fi
    return 1
}

# ── Build and send the daily-info payload (OS + SMART + CPU static info) ──────
send_daily_info() {
    local os_json=$(get_os_info)
    local smart_json=$(get_smart_health)
    IFS='|' read -r cpu_model cpu_cores cpu_speed <<< "$(get_cpu_info)"
    local timestamp=$(date +%s)

    cpu_model=$(echo "$cpu_model" | sed 's/[\\\"]/\\&/g' | tr -d '\n')
    [[ "$cpu_cores" =~ ^[0-9]+$ ]] || cpu_cores=0
    [[ "$cpu_speed"  =~ ^[0-9]+(\.[0-9]+)?$ ]] || cpu_speed="0"

    local payload="{\"agent_key\":\"$AGENT_KEY\",\"timestamp\":$timestamp,"
    payload+="\"daily_info\":{"
    payload+="\"os_info\":$os_json,"
    payload+="\"smart\":$smart_json,"
    payload+="\"cpu_info\":{\"model\":\"$cpu_model\",\"cores\":$cpu_cores,\"speed_mhz\":$cpu_speed}"
    payload+="}}"

    if send_metrics "$payload"; then
        date +%Y-%m-%d > "$DAILY_STAMP_FILE"
        log "INFO: Daily info (OS + SMART + CPU static) sent"
    else
        log "WARN: Failed to send daily info, will retry tomorrow"
    fi
}

# ═════════════════════════════════════════════════════════════════════════════
# UPDATE CHECK — called once per day to api.pinguzo.com/updates.php
# Passes: agent_key, version, os data
# Receives JSON: { "license": {...}, "updates": {"version":"1.2.0","download_url":"..."} }
# ═════════════════════════════════════════════════════════════════════════════

check_for_updates() {
    local os_arch=$(uname -m 2>/dev/null || echo "unknown")
    local os_name=""
    if [ -f /etc/os-release ]; then
        os_name=$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-$NAME}" | sed 's/[\\\"]/\\&/g' | tr -d '\n')
    fi
    [ -z "$os_name" ] && os_name=$(uname -s 2>/dev/null || echo "Linux")

    local hostname=$(hostname 2>/dev/null | sed 's/[\\\"]/\\&/g' | tr -d '\n')

    # Build JSON payload for update check
    local payload="{\"agent_key\":\"$AGENT_KEY\","
    payload+="\"version\":\"$AGENT_VERSION\","
    payload+="\"os\":\"$os_name\","
    payload+="\"os_arch\":\"$os_arch\","
    payload+="\"hostname\":\"$hostname\"}"

    local response
    response=$(curl -s -w "\n%{http_code}" -X POST "$UPDATE_API_URL" \
        -H "Content-Type: application/json" \
        -H "X-Agent-Key: $AGENT_KEY" \
        -d "$payload" \
        --max-time 30 \
        --connect-timeout 10 \
        2>/dev/null)

    local http_code=$(echo "$response" | tail -1)
    local body=$(echo "$response" | head -n -1)

    if [ "$http_code" != "200" ]; then
        log "WARN: Update check failed (HTTP $http_code)"
        return
    fi

    # ── Save license data ─────────────────────────────────────────────────────
    # Extract the full "license" object — may contain nested objects (e.g. edge_servers),
    # so the old single-brace regex approach is not reliable. Use python3 when available.
    local license=""
    if command -v python3 &>/dev/null; then
        license=$(echo "$body" | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    lic = data.get('license')
    if lic and lic != 'null':
        print(json.dumps(lic))
except Exception:
    pass
" 2>/dev/null)
    else
        # Basic fallback — does NOT handle nested objects inside license
        license=$(echo "$body" | grep -o '"license":{[^}]*}' | sed 's/"license"://')
    fi
    if [ -n "$license" ] && [ "$license" != "null" ] && [ "$license" != "{}" ]; then
        mkdir -p "$PINGUZO_DIR"
        echo "$license" > "$PINGUZO_DIR/license.json"
        log "INFO: License data updated"
    fi

    # ── Check for update ──────────────────────────────────────────────────────
    # Extract "updates" object — look for "download_url" key
    local download_url=$(echo "$body" | grep -o '"download_url":"[^"]*"' | sed 's/"download_url":"//;s/"$//')
    local new_version=$(echo "$body"   | grep -o '"version":"[^"]*"'     | sed 's/"version":"//;s/"$//')

    if [ -n "$download_url" ] && [ "$download_url" != "null" ]; then
        log "INFO: Update available: v$new_version — downloading from $download_url"

        local tmp_zip="$PINGUZO_DIR/update.zip"
        local tmp_dir="$PINGUZO_DIR/update_tmp"

        # Download the ZIP
        if curl -s -L -o "$tmp_zip" "$download_url" --max-time 120 --connect-timeout 15 2>/dev/null; then
            # Verify it's actually a ZIP file
            if file "$tmp_zip" 2>/dev/null | grep -qi 'zip\|archive'; then
                mkdir -p "$tmp_dir"
                if unzip -o -d "$tmp_dir" "$tmp_zip" >> "$LOG_FILE" 2>&1; then
                    # Copy new files over (preserve agent.conf and license)
                    rsync -a --exclude='config/agent.conf' --exclude='license.json' \
                        "$tmp_dir/" "$PINGUZO_DIR/" 2>/dev/null || \
                    cp -rf "$tmp_dir/"* "$PINGUZO_DIR/" 2>/dev/null

                    # Ensure scripts are executable
                    find "$PINGUZO_DIR" -name "*.sh" -exec chmod +x {} \; 2>/dev/null

                    log "INFO: Successfully updated to v$new_version"
                    echo "$new_version" > "$PINGUZO_DIR/version.txt"
                else
                    log "ERROR: Failed to unzip update"
                fi
                rm -rf "$tmp_dir"
            else
                log "ERROR: Downloaded file is not a valid ZIP archive"
            fi
            rm -f "$tmp_zip"
        else
            log "ERROR: Failed to download update from $download_url"
        fi
    else
        log "INFO: No update available (current: v$AGENT_VERSION)"
    fi

    # Mark update check done for today
    date +%Y-%m-%d > "$UPDATE_STAMP_FILE"
}

# ═════════════════════════════════════════════════════════════════════════════
# HELPERS
# ═════════════════════════════════════════════════════════════════════════════

# Sanitise a numeric value — return 0 if empty or non-numeric
num() {
    local v=$(echo "$1" | tr -d '[:space:]')
    if [[ "$v" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
        echo "$v"
    else
        echo "0"
    fi
}

# Read failover edge API URLs from license.json (edge_servers field).
# Prints one metrics URL per line; prints nothing if license.json is absent
# or the edge_servers key is missing — signalling that failover is not available.
get_failover_urls() {
    local license_file="$PINGUZO_DIR/license.json"
    [ -f "$license_file" ] || return
    if command -v python3 &>/dev/null; then
        python3 -c "
import json
try:
    with open('$license_file') as f:
        data = json.load(f)
    edges = data.get('edge_servers', {})
    for host in edges.values():
        # Values are already full base URLs (e.g. https://in-mum.pinguzo.com)
        print(str(host).rstrip('/'))
except Exception:
    pass
" 2>/dev/null
    fi
}

# ═════════════════════════════════════════════════════════════════════════════
# COLLECT ALL METRICS (called every minute)
# ═════════════════════════════════════════════════════════════════════════════

collect_metrics() {
    local timestamp=$(date +%s)

    # CPU
    local cpu_usage=$(num "$(get_cpu_usage)")

    # Memory + Swap
    IFS='|' read -r mem_percent mem_used_mb mem_total_mb swap_percent swap_used_mb swap_total_mb <<< "$(get_memory_stats)"
    mem_percent=$(num "$mem_percent"); mem_used_mb=$(num "$mem_used_mb"); mem_total_mb=$(num "$mem_total_mb")
    swap_percent=$(num "$swap_percent"); swap_used_mb=$(num "$swap_used_mb"); swap_total_mb=$(num "$swap_total_mb")

    # Disk usage + I/O (root only for scalars; all partitions sent separately)
    IFS='|' read -r disk_percent disk_read_bytes disk_write_bytes <<< "$(get_disk_stats)"
    disk_percent=$(num "$disk_percent")
    disk_read_bytes=$(num "$disk_read_bytes")
    disk_write_bytes=$(num "$disk_write_bytes")

    # All disks + inodes (base64 JSON arrays — stored server-side, not in VictoriaMetrics)
    local all_disks_b64=$(get_all_disks)
    local all_inodes_b64=$(get_all_inodes)

    # Load averages
    IFS='|' read -r load_1m load_5m load_15m <<< "$(get_load_averages)"
    load_1m=$(num "$load_1m"); load_5m=$(num "$load_5m"); load_15m=$(num "$load_15m")

    # Uptime + open files + SSH sessions
    local uptime_seconds=$(num "$(get_uptime_seconds)")
    local open_files=$(num "$(get_open_files)")
    local ssh_sessions=$(num "$(get_ssh_sessions)")

    # TCP connections
    IFS='|' read -r tcp_established tcp_time_wait tcp_close_wait <<< "$(get_tcp_connections)"
    tcp_established=$(num "$tcp_established"); tcp_time_wait=$(num "$tcp_time_wait"); tcp_close_wait=$(num "$tcp_close_wait")

    # Network — primary interface scalars
    IFS='|' read -r net_iface net_rx_bytes net_tx_bytes net_rx_pkts net_tx_pkts net_rx_errs net_tx_errs <<< "$(get_network_stats)"
    net_iface=$(echo "$net_iface" | sed 's/[\\\"]/\\&/g')
    net_rx_bytes=$(num "$net_rx_bytes"); net_tx_bytes=$(num "$net_tx_bytes")
    net_rx_pkts=$(num "$net_rx_pkts");   net_tx_pkts=$(num "$net_tx_pkts")
    net_rx_errs=$(num "$net_rx_errs");   net_tx_errs=$(num "$net_tx_errs")

    # All interfaces (base64 JSON)
    local all_ifaces_b64=$(get_all_interfaces)

    # IPv4 / IPv6 addresses (base64 JSON)
    local ipv4_b64=$(get_ipv4_addresses)
    local ipv6_b64=$(get_ipv6_addresses)

    # Top processes (base64 JSON — stored server-side in Redis snapshot)
    local top_procs_b64=$(get_top_processes)

    # SSH session details (base64 JSON)
    local ssh_detail_b64=$(get_ssh_session_details)

    # Build JSON payload
    local payload="{\"agent_key\":\"$AGENT_KEY\",\"timestamp\":$timestamp,\"agent_version\":\"$AGENT_VERSION\","
    payload+="\"metrics\":{"
    payload+="\"cpu_percent\":$cpu_usage,"
    payload+="\"memory_percent\":$mem_percent,"
    payload+="\"memory_used_mb\":$mem_used_mb,"
    payload+="\"memory_total_mb\":$mem_total_mb,"
    payload+="\"swap_percent\":$swap_percent,"
    payload+="\"swap_used_mb\":$swap_used_mb,"
    payload+="\"swap_total_mb\":$swap_total_mb,"
    payload+="\"disk_percent\":$disk_percent,"
    payload+="\"disk_read_bytes\":$disk_read_bytes,"
    payload+="\"disk_write_bytes\":$disk_write_bytes,"
    payload+="\"load_average_1m\":$load_1m,"
    payload+="\"load_average_5m\":$load_5m,"
    payload+="\"load_average_15m\":$load_15m,"
    payload+="\"uptime_seconds\":$uptime_seconds,"
    payload+="\"open_files\":$open_files,"
    payload+="\"ssh_sessions\":$ssh_sessions,"
    payload+="\"tcp_established\":$tcp_established,"
    payload+="\"tcp_time_wait\":$tcp_time_wait,"
    payload+="\"tcp_close_wait\":$tcp_close_wait,"
    payload+="\"network_rx_bytes\":$net_rx_bytes,"
    payload+="\"network_tx_bytes\":$net_tx_bytes,"
    payload+="\"network_rx_packets\":$net_rx_pkts,"
    payload+="\"network_tx_packets\":$net_tx_pkts,"
    payload+="\"network_rx_errors\":$net_rx_errs,"
    payload+="\"network_tx_errors\":$net_tx_errs"
    payload+="},"
    # Blob fields — base64-encoded JSON arrays, stored in Redis not VictoriaMetrics
    payload+="\"blobs\":{"
    payload+="\"all_disks\":\"$all_disks_b64\","
    payload+="\"all_inodes\":\"$all_inodes_b64\","
    payload+="\"all_interfaces\":\"$all_ifaces_b64\","
    payload+="\"ipv4\":\"$ipv4_b64\","
    payload+="\"ipv6\":\"$ipv6_b64\","
    payload+="\"top_processes\":\"$top_procs_b64\","
    payload+="\"ssh_sessions_detail\":\"$ssh_detail_b64\""
    payload+="}}"

    echo "$payload"
}

# ═════════════════════════════════════════════════════════════════════════════
# SEND / QUEUE
# ═════════════════════════════════════════════════════════════════════════════

# Low-level: single POST attempt to a given URL. Returns 0 on HTTP 200, 1 otherwise.
_curl_post() {
    local url="$1"
    local payload="$2"
    local response
    response=$(curl -s -w "\n%{http_code}" -X POST "$url" \
        -H "Content-Type: application/json" \
        -H "X-Agent-Key: $AGENT_KEY" \
        -d "$payload" \
        --max-time 10 \
        --connect-timeout 5 \
        --retry 0 \
        2>/dev/null)
    local http_code
    http_code=$(echo "$response" | tail -1)
    [[ "$http_code" = "200" || "$http_code" = "202" ]]
}

# Send metrics payload.
# 1. Tries the configured primary edge (API_BASE + METRICS_ENDPOINT) up to 3 times.
# 2. On failure, reads edge_servers from license.json and tries each failover in turn.
# 3. If edge_servers is absent from license.json, no failover is attempted —
#    the caller (main) is responsible for queuing the payload for later retry.
send_metrics() {
    local payload="$1"

    # ── Primary: up to 3 attempts ─────────────────────────────────────────────
    local retry=0
    while [ $retry -lt 3 ]; do
        if _curl_post "${API_BASE}${METRICS_ENDPOINT}" "$payload"; then
            return 0
        fi
        retry=$((retry + 1))
        [ $retry -lt 3 ] && sleep 1
    done

    # ── Failover: edges from license.json (only if edge_servers key exists) ───
    local license_file="$PINGUZO_DIR/license.json"
    [ -f "$license_file" ] || return 1

    local failover_urls
    failover_urls=$(get_failover_urls)
    [ -z "$failover_urls" ] && return 1   # edge_servers absent → caller queues

    while IFS= read -r failover_url; do
        [ -z "$failover_url" ] && continue
        [ "$failover_url" = "$API_BASE" ] && continue   # already tried above
        if _curl_post "${failover_url}${METRICS_ENDPOINT}" "$payload"; then
            log "INFO: Metrics delivered via failover edge: $failover_url"
            return 0
        fi
    done <<< "$failover_urls"

    return 1
}

# Queue metric for later sending
queue_metric() {
    local payload="$1"
    if [ -f "$QUEUE_FILE" ]; then
        local queue_size=$(wc -l < "$QUEUE_FILE")
        if [ $queue_size -ge $MAX_QUEUE_SIZE ]; then
            tail -900 "$QUEUE_FILE" > "$QUEUE_FILE.tmp"
            mv "$QUEUE_FILE.tmp" "$QUEUE_FILE"
        fi
    fi
    echo "$payload" >> "$QUEUE_FILE"
}

# Send queued metrics as a single batch POST (up to 50 entries per cycle)
# Builds {"batch":[...]} JSON and sends one HTTP request instead of one per entry.
send_queued_metrics() {
    [ ! -f "$QUEUE_FILE" ] && return 0
    [ ! -s "$QUEUE_FILE" ] && return 0

    local tmp_batch; tmp_batch=$(mktemp)
    local count=0
    local total
    total=$(wc -l < "$QUEUE_FILE")

    # Build {"batch":[...]} JSON from up to 50 queued payloads (one JSON object per line)
    printf '{"batch":[' > "$tmp_batch"
    while IFS= read -r line && [ "$count" -lt 50 ]; do
        [ "$count" -gt 0 ] && printf ',' >> "$tmp_batch"
        printf '%s' "$line" >> "$tmp_batch"
        count=$((count + 1))
    done < "$QUEUE_FILE"
    printf ']}' >> "$tmp_batch"

    if [ "$count" -eq 0 ]; then
        rm -f "$tmp_batch"
        return 0
    fi

    # POST the batch file directly (avoids large shell variable for big payloads)
    local response http_code body
    response=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}${METRICS_ENDPOINT}" \
        -H "Content-Type: application/json" \
        -H "X-Agent-Key: $AGENT_KEY" \
        --data "@$tmp_batch" \
        --max-time 15 \
        --connect-timeout 5 \
        --retry 0 \
        2>/dev/null)
    http_code=$(printf '%s' "$response" | tail -1)
    body=$(printf '%s' "$response" | head -n -1)

    rm -f "$tmp_batch"

    if [ "$http_code" = "200" ]; then
        # Remove the entries we just sent; keep any remaining ones
        if [ "$count" -ge "$total" ]; then
            rm -f "$QUEUE_FILE"
        else
            local tmp_remain; tmp_remain=$(mktemp)
            tail -n "+$((count + 1))" "$QUEUE_FILE" > "$tmp_remain"
            mv "$tmp_remain" "$QUEUE_FILE"
        fi
        log "INFO: Sent $count queued metric(s) as a batch"
    else
        log "WARN: Queued batch failed (HTTP ${http_code:-0}) — will retry next cycle | $(printf '%s' "$body" | head -c 200)"
        return 1
    fi
}

# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════

uninstall_agent() {
    log "INFO: Uninstalling Pinguzo agent"

    # Remove cron file
    if [ -f /etc/cron.d/pinguzo ]; then
        rm -f /etc/cron.d/pinguzo
        log "INFO: Removed /etc/cron.d/pinguzo"
    fi

    # Remove agent directory (config, queue, scripts, license)
    if [ -d "$PINGUZO_DIR" ]; then
        rm -rf "$PINGUZO_DIR"
        log "INFO: Removed $PINGUZO_DIR"
    fi

    # Remove log file
    if [ -f "$LOG_FILE" ]; then
        rm -f "$LOG_FILE"
    fi

    # Remove lock file if present
    rm -f "$LOCK_FILE"

    echo "Pinguzo agent uninstalled successfully."
}

main() {
    # ── Handle --uninstall mode ───────────────────────────────────────────────
    if [ "${1:-}" = "--uninstall" ]; then
        uninstall_agent
        exit 0
    fi

    # ── Handle --updates mode ─────────────────────────────────────────────────
    # When called as: pinguzo-agent.sh --updates
    # Only runs the update check (license + agent self-update) then exits.
    # This is invoked by a separate once-daily cron entry, not the per-minute one.
    if [ "${1:-}" = "--updates" ]; then
        log "INFO: Running update check (--updates mode)"
        check_for_updates
        exit 0
    fi

    # ── Normal per-minute run ─────────────────────────────────────────────────

    # Daily task — OS/SMART/CPU static info
    if should_send_daily; then
        send_daily_info
    fi

    # Collect and send current metrics.
    # Only flush the backlog queue if the current payload succeeds —
    # this prevents stale queued data from being sent when the server is unreachable.
    local metrics_payload=$(collect_metrics)

    if send_metrics "$metrics_payload"; then
        # Current upload succeeded — now flush any previously queued payloads
        send_queued_metrics
        exit 0
    else
        queue_metric "$metrics_payload"
        log "WARN: Failed to send metrics, queued for retry"
        exit 1
    fi
}

main "$@"
