package system

import (
	"context"
	"math"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Sampler samples /proc/stat periodically and caches per-core CPU usage
// percentages. Create with New, start the background goroutine with Start, and
// read the latest snapshot with Snapshot.
type Sampler struct {
	statPath string
	interval time.Duration
	mu       sync.RWMutex
	cache    []CoreUsage
	once     sync.Once
}

// NewSampler creates a Sampler that reads statPath and samples every interval.
func NewSampler(statPath string, interval time.Duration) *Sampler {
	return &Sampler{statPath: statPath, interval: interval}
}

// Start launches the background sampling goroutine. Only the first call starts
// it; subsequent calls are no-ops. The goroutine exits when ctx is cancelled.
func (s *Sampler) Start(ctx context.Context) {
	s.once.Do(func() {
		go s.loop(ctx)
	})
}

// Snapshot returns a copy of the latest per-core usage snapshot. Returns nil
// before the first sample completes.
func (s *Sampler) Snapshot() []CoreUsage {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.cache == nil {
		return nil
	}
	out := make([]CoreUsage, len(s.cache))
	copy(out, s.cache)
	return out
}

func (s *Sampler) loop(ctx context.Context) {
	prev := readProcStat(s.statPath)
	ticker := time.NewTicker(s.interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			cur := readProcStat(s.statPath)
			usage := computeUsage(prev, cur)
			s.mu.Lock()
			s.cache = usage
			s.mu.Unlock()
			prev = cur
		}
	}
}

// cpuCoreTicks holds the cumulative jiffies for one "cpuN" line.
type cpuCoreTicks struct {
	core  int
	total uint64
	idle  uint64
}

// readProcStat reads /proc/stat and returns per-core tick totals. The
// aggregate "cpu" line (no digit suffix) is skipped.
func readProcStat(path string) []cpuCoreTicks {
	data, _ := os.ReadFile(path)
	var cores []cpuCoreTicks
	for line := range strings.SplitSeq(string(data), "\n") {
		if !strings.HasPrefix(line, "cpu") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 5 {
			continue
		}
		name := fields[0]
		if name == "cpu" {
			continue
		}
		coreIdx, err := strconv.Atoi(strings.TrimPrefix(name, "cpu"))
		if err != nil {
			continue
		}
		var total, idle uint64
		for _, f := range fields[1:] {
			v, _ := strconv.ParseUint(f, 10, 64)
			total += v
		}
		if len(fields) > 5 {
			v4, _ := strconv.ParseUint(fields[4], 10, 64)
			v5, _ := strconv.ParseUint(fields[5], 10, 64)
			idle = v4 + v5
		} else {
			v4, _ := strconv.ParseUint(fields[4], 10, 64)
			idle = v4
		}
		cores = append(cores, cpuCoreTicks{core: coreIdx, total: total, idle: idle})
	}
	return cores
}

// computeUsage converts two snapshots into per-core usage percentages.
func computeUsage(prev, cur []cpuCoreTicks) []CoreUsage {
	prevMap := make(map[int]cpuCoreTicks, len(prev))
	for _, c := range prev {
		prevMap[c.core] = c
	}
	usage := make([]CoreUsage, 0, len(cur))
	for _, c := range cur {
		p, ok := prevMap[c.core]
		if !ok {
			continue
		}
		dTotal := c.total - p.total
		dIdle := c.idle - p.idle
		var pct float64
		if dTotal > 0 {
			pct = float64(dTotal-dIdle) / float64(dTotal) * 100
			pct = math.Round(pct*10) / 10
		}
		usage = append(usage, CoreUsage{Core: c.core, UsagePct: pct})
	}
	return usage
}
