// Agen ringan Go: heartbeat + metrik (Linux /proc) ke mesin deteksi. Hanya stdlib, konsumsi sumber daya rendah.
// Build: go build -o csm-agent .   Jalankan: SENSOR_CODE=... SENSOR_KEY=... ./csm-agent -interval 30s
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"
)

type event map[string]any

func cpuTimes() (busy, total uint64, ok bool) {
	b, err := os.ReadFile("/proc/stat")
	if err != nil {
		return 0, 0, false
	}
	f := strings.Fields(strings.SplitN(string(b), "\n", 2)[0])
	if len(f) < 8 || f[0] != "cpu" {
		return 0, 0, false
	}
	var v [7]uint64
	for i := 0; i < 7; i++ {
		v[i], _ = strconv.ParseUint(f[i+1], 10, 64)
		total += v[i]
	}
	return total - v[3] - v[4], total, true
}

func memPercent() (float64, bool) {
	b, err := os.ReadFile("/proc/meminfo")
	if err != nil {
		return 0, false
	}
	var t, a float64
	for _, l := range strings.Split(string(b), "\n") {
		f := strings.Fields(l)
		if len(f) < 2 {
			continue
		}
		v, _ := strconv.ParseFloat(f[1], 64)
		switch f[0] {
		case "MemTotal:":
			t = v
		case "MemAvailable:":
			a = v
		}
	}
	if t == 0 {
		return 0, false
	}
	return 100 * (t - a) / t, true
}

func diskPercent(path string) (float64, bool) {
	var s syscall.Statfs_t
	if err := syscall.Statfs(path, &s); err != nil || s.Blocks == 0 {
		return 0, false
	}
	return 100 * float64(s.Blocks-s.Bavail) / float64(s.Blocks), true
}

func collect() []event {
	evs := []event{{"type": "heartbeat"}}
	if runtime.GOOS != "linux" {
		return evs // di OS lain hanya heartbeat; gunakan sensor PowerShell untuk Windows
	}
	m := map[string]float64{}
	b1, t1, ok1 := cpuTimes()
	time.Sleep(time.Second)
	b2, t2, ok2 := cpuTimes()
	if ok1 && ok2 && t2 > t1 {
		m["cpu"] = 100 * float64(b2-b1) / float64(t2-t1)
	}
	if v, ok := memPercent(); ok {
		m["mem"] = v
	}
	if v, ok := diskPercent("/"); ok {
		m["disk"] = v
	}
	if len(m) > 0 {
		evs = append(evs, event{"type": "metrics", "metrics": m})
	}
	return evs
}

func send(client *http.Client, url, code, key string, evs []event) error {
	host, _ := os.Hostname()
	body, _ := json.Marshal(map[string]any{"sensor_code": code, "hostname": host, "os": runtime.GOOS, "events": evs})
	req, err := http.NewRequest(http.MethodPost, url+"/api/v1/ingest", bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Sensor-Key", key)
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("mesin menolak (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(out)))
	}
	log.Printf("terkirim: %s", strings.TrimSpace(string(out)))
	return nil
}

func main() {
	interval := flag.Duration("interval", 30*time.Second, "interval pengiriman")
	once := flag.Bool("once", false, "kirim sekali lalu keluar")
	flag.Parse()
	url := os.Getenv("ENGINE_URL")
	if url == "" {
		url = "http://127.0.0.1:8000"
	}
	code, key := os.Getenv("SENSOR_CODE"), os.Getenv("SENSOR_KEY")
	if code == "" || key == "" {
		log.Fatal("SENSOR_CODE dan SENSOR_KEY wajib diisi (environment variable)")
	}
	client := &http.Client{Timeout: 10 * time.Second}
	for {
		if err := send(client, url, code, key, collect()); err != nil {
			log.Printf("gagal kirim: %v", err)
			if *once {
				os.Exit(1)
			}
		}
		if *once {
			return
		}
		time.Sleep(*interval)
	}
}
