#!/usr/bin/env bash
# monitor.sh — pemantau resource sederhana untuk admin kecil.
#
# Setiap eksekusi mengumpulkan tiga metrik — load average 1 menit,
# persentase memori terpakai, dan persentase disk terpakai per mount
# point dari konfigurasi — membandingkannya dengan ambang waspada dan
# kritis dari berkas konfigurasi, mencetak laporan berbentuk tabel,
# menulis log terstruktur, dan mengirim alert ke stderr setelah satu
# metrik melewati ambang beberapa kali berturut-turut. Berjalan sebagai
# user biasa — tidak butuh root.
#
# Pemakaian:
#   bash monitor.sh [--data-dir DIR]
#
#   --data-dir DIR  baca loadavg, meminfo, dan df.txt dari DIR alih-
#                   alih dari /proc dan df sungguhan (untuk pengujian).
#
# Variabel lingkungan (berguna untuk pengujian):
#   MONITOR_CONF    berkas konfigurasi yang dibaca (default: monitor.conf)
#   MONITOR_SOURCE  sama dengan --data-dir (opsi menang bila keduanya diisi)
#   MONITOR_STAMP   stempel waktu ISO untuk log (default: waktu sekarang)
#
# Kode keluar:
#   0 semua metrik OK        1 ada metrik WASPADA
#   2 ada metrik KRITIS      3 konfigurasi atau sumber data tidak valid

set -u

CONF="${MONITOR_CONF:-monitor.conf}"
STAMP="${MONITOR_STAMP:-$(date +%Y-%m-%dT%H:%M:%S%:z)}"
data_dir="${MONITOR_SOURCE:-}"

usage() {
    printf 'Pemakaian: bash monitor.sh [--data-dir DIR]\n'
    printf '\n'
    printf 'Memeriksa load average, memori, dan disk menurut ambang di\n'
    printf 'monitor.conf, mencetak laporan, menulis log, dan mengirim\n'
    printf 'alert setelah satu metrik melewati ambang beberapa kali\n'
    printf 'berturut-turut.\n'
    printf '\n'
    printf '  --data-dir DIR  baca data tiruan dari DIR (untuk pengujian)\n'
    printf '  -h, --help      tampilkan bantuan ini\n'
    printf '\n'
    printf 'Kode keluar: 0 semua OK, 1 ada WASPADA, 2 ada KRITIS,\n'
    printf '3 konfigurasi atau sumber data tidak valid.\n'
}

while [ $# -gt 0 ]; do
    case "$1" in
        --data-dir)
            if [ $# -lt 2 ]; then
                printf 'monitor.sh: --data-dir membutuhkan argumen DIR\n' >&2
                exit 3
            fi
            data_dir=$2
            shift 2
            ;;
        --data-dir=*)
            data_dir=${1#--data-dir=}
            shift
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            printf 'monitor.sh: argumen tidak dikenal: %s\n' "$1" >&2
            exit 3
            ;;
    esac
done

# angka_desimal V: sukses bila V bilangan desimal sederhana seperti
# "1.50" atau "3" — tanpa tanda, tanpa titik ganda, tanpa titik di ujung.
angka_desimal() {
    case "$1" in
        ''|*[!0-9.]*) return 1 ;;
        *.*.*)        return 1 ;;
        .*|*.|*.)     return 1 ;;
    esac
    return 0
}

# angka_persen V: sukses bila V bilangan bulat 1 sampai 99.
angka_persen() {
    case "$1" in
        ''|*[!0-9]*) return 1 ;;
    esac
    [ "$1" -ge 1 ] && [ "$1" -le 99 ]
}

# angka_bulat V: sukses bila V bilangan bulat 1 atau lebih.
angka_bulat() {
    case "$1" in
        ''|*[!0-9]*|0) return 1 ;;
    esac
    return 0
}

# --- baca dan validasi konfigurasi ---------------------------------------
if [ ! -f "$CONF" ]; then
    printf 'monitor.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
    exit 3
fi
# shellcheck source=/dev/null
. "$CONF"

gagal_konfig() {
    printf 'monitor.sh: %s (periksa %s)\n' "$1" "$CONF" >&2
    exit 3
}

angka_desimal "${LOAD_WARN:-}"     || gagal_konfig 'LOAD_WARN harus angka desimal, mis. 1.50'
angka_desimal "${LOAD_KRITIS:-}"   || gagal_konfig 'LOAD_KRITIS harus angka desimal, mis. 3.00'
angka_persen  "${MEM_WARN_PCT:-}"    || gagal_konfig 'MEM_WARN_PCT harus bilangan bulat 1-99'
angka_persen  "${MEM_KRITIS_PCT:-}"  || gagal_konfig 'MEM_KRITIS_PCT harus bilangan bulat 1-99'
angka_persen  "${DISK_WARN_PCT:-}"   || gagal_konfig 'DISK_WARN_PCT harus bilangan bulat 1-99'
angka_persen  "${DISK_KRITIS_PCT:-}" || gagal_konfig 'DISK_KRITIS_PCT harus bilangan bulat 1-99'
angka_bulat   "${ALERT_REPEAT:-}"    || gagal_konfig 'ALERT_REPEAT harus bilangan bulat 1 atau lebih'
angka_bulat   "${LOG_KEEP:-}"        || gagal_konfig 'LOG_KEEP harus bilangan bulat 1 atau lebih'
angka_bulat   "${LOG_MAX_BYTES:-}"   || gagal_konfig 'LOG_MAX_BYTES harus bilangan bulat 1 atau lebih'
[ -n "${DISK_MOUNTS:-}" ] || gagal_konfig 'DISK_MOUNTS belum diisi'
[ -n "${LOG_FILE:-}" ]   || gagal_konfig 'LOG_FILE belum diisi'

if ! awk -v a="$LOAD_WARN" -v b="$LOAD_KRITIS" 'BEGIN { exit (a < b) ? 0 : 1 }'; then
    gagal_konfig 'LOAD_WARN harus lebih kecil dari LOAD_KRITIS'
fi
[ "${MEM_WARN_PCT}" -lt "${MEM_KRITIS_PCT}" ] ||
    gagal_konfig 'MEM_WARN_PCT harus lebih kecil dari MEM_KRITIS_PCT'
[ "${DISK_WARN_PCT}" -lt "${DISK_KRITIS_PCT}" ] ||
    gagal_konfig 'DISK_WARN_PCT harus lebih kecil dari DISK_KRITIS_PCT'

# --- sumber data: /proc sungguhan atau fixture pengujian ------------------
if [ -n "$data_dir" ]; then
    loadavg_file="$data_dir/loadavg"
    meminfo_file="$data_dir/meminfo"
    df_file="$data_dir/df.txt"
    for f in "$loadavg_file" "$meminfo_file" "$df_file"; do
        if [ ! -r "$f" ]; then
            printf 'monitor.sh: sumber data %s tidak terbaca\n' "$f" >&2
            exit 3
        fi
    done
    df_output=$(cat "$df_file")
else
    loadavg_file=/proc/loadavg
    meminfo_file=/proc/meminfo
    df_output=$(df -P)
fi

# --- siapkan berkas log, state, dan rotasi --------------------------------
LOG_DIR=$(dirname "$LOG_FILE")
mkdir -p "$LOG_DIR" || exit 3
STATE_FILE="$LOG_DIR/monitor.state"
STATE_TMP="$STATE_FILE.tmp"

if [ -f "$LOG_FILE" ]; then
    ukuran=$(stat -c %s "$LOG_FILE")
    if [ "$ukuran" -gt "$LOG_MAX_BYTES" ]; then
        i=$LOG_KEEP
        while [ "$i" -gt 1 ]; do
            [ -f "$LOG_FILE.$((i - 1))" ] &&
                mv "$LOG_FILE.$((i - 1))" "$LOG_FILE.$i"
            i=$((i - 1))
        done
        mv "$LOG_FILE" "$LOG_FILE.1"
    fi
fi

# --- fungsi pengumpul data ------------------------------------------------

# ambil_load100: load average 1 menit dikali 100 (2.50 menjadi 250).
ambil_load100() {
    awk '{ printf "%.0f", $1 * 100 }' "$loadavg_file"
}

# ke_ratus ANGKA: kalikan angka desimal dengan 100 menjadi bilangan bulat.
ke_ratus() {
    awk -v v="$1" 'BEGIN { printf "%.0f", v * 100 }'
}

# ambil_mem100: persen memori terpakai. MemAvailable dipakai karena sudah
# memperhitungkan cache yang bisa direklamasi — bukan sekadar MemFree.
ambil_mem100() {
    awk '
        $1 == "MemTotal:"     { total = $2 }
        $1 == "MemAvailable:" { avail = $2; ada = 1 }
        END { if (total > 0 && ada) printf "%.0f", (total - avail) * 100 / total }
    ' "$meminfo_file"
}

# ambil_disk100 MOUNT: persen terpakai untuk MOUNT dari output df.
ambil_disk100() {
    printf '%s\n' "$df_output" |
        awk -v mnt="$1" '
            $6 == mnt { gsub(/%/, "", $5); print $5 + 0; exit }
        '
}

# --- fungsi status ---------------------------------------------------------

# kode_status NILAI WASPADA KRITIS: 0=OK, 1=WASPADA, 2=KRITIS.
# Nilai dan kedua ambang harus bilangan bulat.
kode_status() {
    if [ "$1" -ge "$3" ]; then
        printf '2'
    elif [ "$1" -ge "$2" ]; then
        printf '1'
    else
        printf '0'
    fi
}

# nama_status KODE: bentuk teks dari kode status.
nama_status() {
    case "$1" in
        1) printf 'WASPADA' ;;
        2) printf 'KRITIS' ;;
        *) printf 'OK' ;;
    esac
}

# --- fungsi log, state, dan alert ------------------------------------------

# catat STATUS METRIK NILAI: tambah satu baris log terstruktur.
catat() {
    printf '%s %-7s %-11s %s\n' "$STAMP" "$1" "$2" "$3" >> "$LOG_FILE"
}

# baca_hitung KUNCI: berapa kali berturut-turut KUNCI melewati ambang.
baca_hitung() {
    [ -f "$STATE_FILE" ] || return 0
    awk -v k="$1" '$1 == k { print $2; exit }' "$STATE_FILE"
}

# warna_status STATUS: teks status, diberi warna ANSI hanya bila stdout
# adalah terminal — output yang dialihkan ke berkas tetap polos.
warna_status() {
    if [ -t 1 ]; then
        case "$1" in
            WASPADA) printf '\033[33m%s\033[0m' "$1" ;;
            KRITIS)  printf '\033[31m%s\033[0m' "$1" ;;
            *)       printf '%s' "$1" ;;
        esac
    else
        printf '%s' "$1"
    fi
}

# --- kumpulkan data --------------------------------------------------------

load_tampil=$(awk '{ print $1 }' "$loadavg_file")
load100=$(ambil_load100)
if [ -z "$load100" ]; then
    printf 'monitor.sh: %s tidak berisi load average\n' "$loadavg_file" >&2
    exit 3
fi

mem100=$(ambil_mem100)
if [ -z "$mem100" ]; then
    printf 'monitor.sh: %s tidak berisi MemTotal/MemAvailable\n' "$meminfo_file" >&2
    exit 3
fi

load_warn100=$(ke_ratus "$LOAD_WARN")
load_kritis100=$(ke_ratus "$LOAD_KRITIS")

# Mount yang tidak ada di data df ditolak di sini, sebelum laporan
# dicetak, supaya tabel tidak tercetak setengah jalan.
# shellcheck disable=SC2086  # daftar mount memang dipisah spasi
for mnt in $DISK_MOUNTS; do
    if [ -z "$(ambil_disk100 "$mnt")" ]; then
        printf 'monitor.sh: mount %s tidak ditemukan di data df (periksa DISK_MOUNTS)\n' "$mnt" >&2
        exit 3
    fi
done

# --- periksa tiap metrik: laporan + log + alert ----------------------------

terburuk=0
: > "$STATE_TMP"

# periksa METRIK TAMPIL NILAI WASPADA KRITIS LABEL_AMBANG
periksa() {
    local metrik=$1 tampil=$2 nilai=$3 warn=$4 kritis=$5 label=$6
    local kode status n

    kode=$(kode_status "$nilai" "$warn" "$kritis")
    status=$(nama_status "$kode")
    if (( kode > terburuk )); then
        terburuk=$kode
    fi

    printf '%-10s %7s %-12s %s\n' "$metrik" "$tampil" "$label" \
        "$(warna_status "$status")"
    catat "$status" "$metrik" "$tampil"

    n=$(baca_hitung "$metrik")
    n=${n:-0}
    if [ "$status" != "OK" ]; then
        n=$((n + 1))
        printf '%s %s\n' "$metrik" "$n" >> "$STATE_TMP"
        if [ "$n" -eq "$ALERT_REPEAT" ]; then
            printf 'monitor.sh: ALERT %s = %s melewati ambang %sx berturut-turut\n' \
                "$metrik" "$tampil" "$ALERT_REPEAT" >&2
            catat ALERT "$metrik" "$tampil"
        fi
    fi
}

printf '%-10s %7s %-12s %s\n' 'Metrik' 'Nilai' 'Ambang W/K' 'Status'
printf '%.0s-' {1..39}; printf '\n'

periksa cpu-load "$load_tampil" "$load100" "$load_warn100" "$load_kritis100" \
    "$LOAD_WARN/$LOAD_KRITIS"
periksa memori "${mem100}%" "$mem100" "$MEM_WARN_PCT" "$MEM_KRITIS_PCT" \
    "${MEM_WARN_PCT}%/${MEM_KRITIS_PCT}%"

# shellcheck disable=SC2086  # daftar mount memang dipisah spasi
for mnt in $DISK_MOUNTS; do
    disk100=$(ambil_disk100 "$mnt")
    periksa "disk:$mnt" "${disk100}%" "$disk100" \
        "$DISK_WARN_PCT" "$DISK_KRITIS_PCT" \
        "${DISK_WARN_PCT}%/${DISK_KRITIS_PCT}%"
done

printf 'kesimpulan: %s\n' "$(nama_status "$terburuk")"

mv "$STATE_TMP" "$STATE_FILE"
exit "$terburuk"
