#!/usr/bin/env bash
# =============================================================================
# onx-cgroup-stat-read — Bulk per-customer cgroup stat reader (Agent 4 v86.5)
#
# Sister script of onx-cgroup-usage-read. Daha geniş diagnostic payload:
#   - cpu.stat tüm alanları (usage/user/system/throttled)
#   - memory.* tüm değerleri (current/peak/swap.current/max/high)
#   - io.stat hem byte hem ops aggregation
#   - pids.current/peak/max
#   - cgroup.procs ile total_process_count
#   - /proc/<PID>/comm ile fpm_worker_count
#
# Input (stdin JSON):
#   {"username":"onx_leafport"}   # optional; missing → bulk mode (all users)
#
# Output (stdout JSON):
#   {
#     "ok": true,
#     "snapshot_at_unix": 1748275200,
#     "per_user": [
#       {
#         "username": "onx_leafport",
#         "slice_exists": true,
#         "cpu": {
#           "usage_usec": 12345678,
#           "user_usec": 9876543,
#           "system_usec": 2469135,
#           "nr_periods": 100,
#           "nr_throttled": 5,
#           "throttled_usec": 50000
#         },
#         "memory": {
#           "current": 524288000,
#           "peak": 612343296,
#           "swap_current": 0,
#           "max": 2147483648,
#           "high": 1610612736
#         },
#         "io": {
#           "rbytes": 1048576,
#           "wbytes": 5242880,
#           "rios": 100,
#           "wios": 250
#         },
#         "pids": {
#           "current": 12,
#           "peak": 25,
#           "max": 1024
#         },
#         "process_count": 12,
#         "fpm_worker_count": 10
#       }
#     ],
#     "total_users": 1,
#     "elapsed_ms": 45
#   }
#
# Slice missing → slice_exists:false, all numeric fields 0.
# "max" sentinel (memory.max / pids.max) → 0.
#
# Exit codes:
#   0  success (even with empty bulk result)
#   1  invalid input (bad JSON / invalid username)
#   2  preflight fail (jq missing / non-Linux)
#   3  internal error
#
# Performance: ~10ms/user × 50 users = 500ms total target.
# Read-only — yapısı gereği DB/cgroup writeback ASLA yapmaz.
#
# Deployed to: /usr/local/onoxsoft/bin/onx-cgroup-stat-read
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

command -v jq >/dev/null 2>&1 || { printf '{"error":"jq required"}\n' >&2; exit 2; }

readonly CGROUP_ROOT="/sys/fs/cgroup/customer.slice"

# ── Stopwatch (ms) ───────────────────────────────────────────────────────────
T_START_NS="$(date +%s%N)"

# ── Read stdin (optional — bulk mode if empty) ───────────────────────────────
INPUT="$(cat 2>/dev/null || true)"
[[ -z "${INPUT}" ]] && INPUT='{}'

echo "${INPUT}" | jq -e 'type == "object"' >/dev/null 2>&1 \
    || onx_die 1 "stdin is not a valid JSON object"

USERNAME="$(onx_json_get "${INPUT}" "username")"

# ─────────────────────────────────────────────────────────────────────────────
# Helpers — sliced read into integer (or 0 fallback).
# ─────────────────────────────────────────────────────────────────────────────

# Single-int file (memory.current, memory.peak, etc.).
# "max" → 0; multi-token line → first integer; missing → 0.
_read_int_file() {
    local path="${1}"
    local default="${2:-0}"
    local val
    [[ -r "${path}" ]] || { printf '%s' "${default}"; return; }
    read -r val < "${path}" 2>/dev/null || val=""
    if [[ -z "${val}" || "${val}" == "max" ]]; then
        printf '%s' "${default}"
    elif [[ "${val}" =~ ^[0-9]+$ ]]; then
        printf '%s' "${val}"
    else
        local first
        first="$(printf '%s' "${val}" | awk '{print $1+0}')"
        printf '%s' "${first:-${default}}"
    fi
}

# Read cpu.stat → emit 6 integers space-separated:
#   usage_usec user_usec system_usec nr_periods nr_throttled throttled_usec
_read_cpu_stat() {
    local path="${1}/cpu.stat"
    if [[ ! -r "${path}" ]]; then
        printf '0 0 0 0 0 0'
        return
    fi
    awk '
        BEGIN {
            u=0; us=0; sy=0; p=0; t=0; tu=0
        }
        $1=="usage_usec"     {u=$2}
        $1=="user_usec"      {us=$2}
        $1=="system_usec"    {sy=$2}
        $1=="nr_periods"     {p=$2}
        $1=="nr_throttled"   {t=$2}
        $1=="throttled_usec" {tu=$2}
        END { printf("%d %d %d %d %d %d", u+0, us+0, sy+0, p+0, t+0, tu+0) }
    ' "${path}" 2>/dev/null || printf '0 0 0 0 0 0'
}

# Read io.stat → emit "rbytes wbytes rios wios" aggregated across all devs.
_read_io_stat() {
    local path="${1}/io.stat"
    if [[ ! -r "${path}" ]]; then
        printf '0 0 0 0'
        return
    fi
    awk '
        BEGIN { rb=0; wb=0; ri=0; wi=0 }
        {
            for (i=2; i<=NF; i++) {
                if      ($i ~ /^rbytes=/) { sub(/^rbytes=/,"",$i); rb += $i }
                else if ($i ~ /^wbytes=/) { sub(/^wbytes=/,"",$i); wb += $i }
                else if ($i ~ /^rios=/)   { sub(/^rios=/,"",$i);   ri += $i }
                else if ($i ~ /^wios=/)   { sub(/^wios=/,"",$i);   wi += $i }
            }
        }
        END { printf("%d %d %d %d", rb+0, wb+0, ri+0, wi+0) }
    ' "${path}" 2>/dev/null || printf '0 0 0 0'
}

# total process count from cgroup.procs (line count)
_read_proc_count() {
    local path="${1}/cgroup.procs"
    [[ -r "${path}" ]] || { printf '0'; return; }
    local n
    n="$(wc -l < "${path}" 2>/dev/null || printf '0')"
    printf '%d' "${n:-0}"
}

# Count FPM workers: PIDs whose /proc/<PID>/comm begins with "php-fpm".
_read_fpm_count() {
    local path="${1}/cgroup.procs"
    [[ -r "${path}" ]] || { printf '0'; return; }
    local n=0
    local pid comm
    while IFS= read -r pid; do
        [[ -z "${pid}" ]] && continue
        # /proc/<PID>/comm contains the process name truncated to 15 chars + newline
        if [[ -r "/proc/${pid}/comm" ]]; then
            read -r comm < "/proc/${pid}/comm" 2>/dev/null || comm=""
            case "${comm}" in
                php-fpm*) n=$(( n + 1 )) ;;
            esac
        fi
    done < "${path}"
    printf '%d' "${n}"
}

# ─────────────────────────────────────────────────────────────────────────────
# Build a per-user JSON object (one element of `per_user` array).
# Args: $1 = username
# Stdout: jq -nc compact JSON.
# ─────────────────────────────────────────────────────────────────────────────
build_user_json() {
    local user="${1}"
    local dir="${CGROUP_ROOT}/customer-${user}.slice"

    if [[ ! -d "${dir}" ]]; then
        # Slice absent → exists:false, all zeros.
        jq -nc --arg u "${user}" '{
            username:         $u,
            slice_exists:     false,
            cpu:    { usage_usec:0, user_usec:0, system_usec:0, nr_periods:0, nr_throttled:0, throttled_usec:0 },
            memory: { current:0, peak:0, swap_current:0, max:0, high:0 },
            io:     { rbytes:0, wbytes:0, rios:0, wios:0 },
            pids:   { current:0, peak:0, max:0 },
            process_count:    0,
            fpm_worker_count: 0
        }'
        return
    fi

    # ── CPU ──
    local cpu_stat
    cpu_stat="$(_read_cpu_stat "${dir}")"
    read -r CPU_U CPU_US CPU_SY CPU_P CPU_T CPU_TU <<<"${cpu_stat}"

    # ── Memory ──
    local mem_cur mem_peak mem_swap mem_max mem_high
    mem_cur="$(_read_int_file "${dir}/memory.current" 0)"
    mem_peak="$(_read_int_file "${dir}/memory.peak" 0)"
    mem_swap="$(_read_int_file "${dir}/memory.swap.current" 0)"
    mem_max="$(_read_int_file "${dir}/memory.max" 0)"
    mem_high="$(_read_int_file "${dir}/memory.high" 0)"

    # ── IO ──
    local io_stat
    io_stat="$(_read_io_stat "${dir}")"
    read -r IO_R IO_W IO_RI IO_WI <<<"${io_stat}"

    # ── PIDs ──
    local pids_cur pids_peak pids_max
    pids_cur="$(_read_int_file "${dir}/pids.current" 0)"
    pids_peak="$(_read_int_file "${dir}/pids.peak" 0)"
    pids_max="$(_read_int_file "${dir}/pids.max" 0)"

    # ── Process breakdown ──
    local proc_count fpm_count
    proc_count="$(_read_proc_count "${dir}")"
    fpm_count="$(_read_fpm_count "${dir}")"

    jq -nc \
        --arg u "${user}" \
        --argjson cu "${CPU_U:-0}" --argjson cus "${CPU_US:-0}" --argjson csy "${CPU_SY:-0}" \
        --argjson cp "${CPU_P:-0}" --argjson ct "${CPU_T:-0}"  --argjson ctu "${CPU_TU:-0}" \
        --argjson mc "${mem_cur:-0}" --argjson mp "${mem_peak:-0}" --argjson ms "${mem_swap:-0}" \
        --argjson mm "${mem_max:-0}" --argjson mh "${mem_high:-0}" \
        --argjson ir "${IO_R:-0}" --argjson iw "${IO_W:-0}" \
        --argjson iri "${IO_RI:-0}" --argjson iwi "${IO_WI:-0}" \
        --argjson pc "${pids_cur:-0}" --argjson pk "${pids_peak:-0}" --argjson pm "${pids_max:-0}" \
        --argjson prc "${proc_count:-0}" --argjson fpm "${fpm_count:-0}" \
        '{
            username:         $u,
            slice_exists:     true,
            cpu: {
                usage_usec:     $cu,
                user_usec:      $cus,
                system_usec:    $csy,
                nr_periods:     $cp,
                nr_throttled:   $ct,
                throttled_usec: $ctu
            },
            memory: {
                current:      $mc,
                peak:         $mp,
                swap_current: $ms,
                max:          $mm,
                high:         $mh
            },
            io: {
                rbytes: $ir,
                wbytes: $iw,
                rios:   $iri,
                wios:   $iwi
            },
            pids: {
                current: $pc,
                peak:    $pk,
                max:     $pm
            },
            process_count:    $prc,
            fpm_worker_count: $fpm
        }'
}

# ── Mode dispatch ────────────────────────────────────────────────────────────

PER_USER_JSON='[]'
TOTAL_USERS=0
SNAPSHOT_AT_UNIX="$(date +%s)"

if [[ -n "${USERNAME}" ]]; then
    # ── SINGLE mode ──────────────────────────────────────────────────────────
    onx_validate_username "${USERNAME}"
    USER_JSON="$(build_user_json "${USERNAME}")"
    PER_USER_JSON="$(jq -nc --argjson u "${USER_JSON}" '[$u]')"
    TOTAL_USERS=1
else
    # ── BULK mode ────────────────────────────────────────────────────────────
    if [[ -d "${CGROUP_ROOT}" ]]; then
        shopt -s nullglob
        SLICE_DIRS=( "${CGROUP_ROOT}"/customer-*.slice )
        shopt -u nullglob

        # Accumulator
        ITEMS=()
        for SLICE_DIR in "${SLICE_DIRS[@]}"; do
            BASE="$(basename "${SLICE_DIR}")"
            USER_PART="${BASE#customer-}"
            USER_PART="${USER_PART%.slice}"

            # Defensive: skip malformed entries.
            [[ "${USER_PART}" =~ ^onx_[a-z0-9]{4,12}$ ]] || continue

            U_JSON="$(build_user_json "${USER_PART}")"
            ITEMS+=( "${U_JSON}" )
            TOTAL_USERS=$(( TOTAL_USERS + 1 ))
        done

        if (( ${#ITEMS[@]} > 0 )); then
            # Build [obj, obj, ...] from accumulator using jq -s.
            PER_USER_JSON="$(printf '%s\n' "${ITEMS[@]}" | jq -sc '.')"
        fi
    fi
fi

# ── Elapsed (ms) ─────────────────────────────────────────────────────────────
T_END_NS="$(date +%s%N)"
ELAPSED_MS=$(( (T_END_NS - T_START_NS) / 1000000 ))

onx_log "cgroup-stat-read: users=${TOTAL_USERS} elapsed_ms=${ELAPSED_MS}"

# ── Output ───────────────────────────────────────────────────────────────────
jq -nc \
    --argjson per "${PER_USER_JSON}" \
    --argjson at "${SNAPSHOT_AT_UNIX}" \
    --argjson total "${TOTAL_USERS}" \
    --argjson ems "${ELAPSED_MS}" \
    '{
        ok:               true,
        snapshot_at_unix: $at,
        per_user:         $per,
        total_users:      $total,
        elapsed_ms:       $ems
    }'

exit 0
