package wshub

import (
	"math"
	"sync"
)

const statRingSize = 512

// SourceStat accumulates per-source UDP performance metrics.
// Thread-safe; RecordFragment is called from the UDPClient goroutine.
type SourceStat struct {
	mu sync.Mutex

	seenFirst   bool
	lastCounter uint32
	TotalRx     uint64
	TotalLost   uint64

	// Cycle-time ring (seconds between consecutive DATA completions)
	ctRing   [statRingSize]float64
	ctHead   int
	ctFull   bool
	lastRxNs int64

	// Per-cycle accumulators (reset after each DATA completion)
	fragCount int
	byteCount int

	fragRing [statRingSize]int
	byteRing [statRingSize]int
}

// RecordFragment is called for every UDP datagram of a DATA packet.
//   complete: this fragment completed the DATA reassembly.
//   nBytes:   raw datagram size (header+payload).
func (s *SourceStat) RecordFragment(counter uint32, nBytes int, arrivalNs int64, complete bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.fragCount++
	s.byteCount += nBytes

	if !complete {
		return
	}

	s.TotalRx++
	if s.seenFirst {
		if delta := counter - s.lastCounter; delta > 1 {
			s.TotalLost += uint64(delta - 1)
		}
	} else {
		s.seenFirst = true
	}
	s.lastCounter = counter

	if s.lastRxNs != 0 {
		idx := s.ctHead
		s.ctRing[idx] = float64(arrivalNs-s.lastRxNs) * 1e-9
		s.fragRing[idx] = s.fragCount
		s.byteRing[idx] = s.byteCount
		s.ctHead = (s.ctHead + 1) % statRingSize
		if s.ctHead == 0 {
			s.ctFull = true
		}
	}
	s.lastRxNs = arrivalNs
	s.fragCount = 0
	s.byteCount = 0
}

// Snapshot computes and returns a StatInfo for broadcast.
func (s *SourceStat) Snapshot() StatInfo {
	s.mu.Lock()
	defer s.mu.Unlock()

	n := s.ctHead
	if s.ctFull {
		n = statRingSize
	}

	si := StatInfo{TotalReceived: s.TotalRx, TotalLost: s.TotalLost}
	if n == 0 {
		return si
	}

	sum, sumSq := 0.0, 0.0
	minV, maxV := math.MaxFloat64, 0.0
	fragSum, byteSum := 0, 0
	for i := 0; i < n; i++ {
		v := s.ctRing[i]
		sum += v
		sumSq += v * v
		if v < minV {
			minV = v
		}
		if v > maxV {
			maxV = v
		}
		fragSum += s.fragRing[i]
		byteSum += s.byteRing[i]
	}
	avg := sum / float64(n)
	variance := sumSq/float64(n) - avg*avg
	if variance < 0 {
		variance = 0
	}
	stdv := math.Sqrt(variance)

	si.CycleAvgMs = avg * 1e3
	si.CycleStdMs = stdv * 1e3
	si.CycleMinMs = minV * 1e3
	si.CycleMaxMs = maxV * 1e3
	si.RateHz = 1.0 / avg
	si.RateStdHz = stdv / (avg * avg)
	si.FragsPerCycle = float64(fragSum) / float64(n)
	si.BytesPerCycle = float64(byteSum) / float64(n)

	const nBins = 20
	si.CycleHistMin = minV * 1e3
	si.CycleHistMax = maxV * 1e3
	si.CycleHist = make([]int, nBins)
	span := maxV - minV
	for i := 0; i < n; i++ {
		var bin int
		if span > 0 {
			bin = int((s.ctRing[i] - minV) / span * float64(nBins))
			if bin >= nBins {
				bin = nBins - 1
			}
		} else {
			bin = nBins / 2
		}
		si.CycleHist[bin]++
	}
	return si
}

// StatInfo is the JSON snapshot for one source sent to the frontend.
type StatInfo struct {
	TotalReceived uint64  `json:"totalReceived"`
	TotalLost     uint64  `json:"totalLost"`
	RateHz        float64 `json:"rateHz"`
	RateStdHz     float64 `json:"rateStdHz"`
	FragsPerCycle float64 `json:"fragsPerCycle"`
	BytesPerCycle float64 `json:"bytesPerCycle"`
	CycleAvgMs    float64 `json:"cycleAvgMs"`
	CycleStdMs    float64 `json:"cycleStdMs"`
	CycleMinMs    float64 `json:"cycleMinMs"`
	CycleMaxMs    float64 `json:"cycleMaxMs"`
	CycleHist     []int   `json:"cycleHist"`
	CycleHistMin  float64 `json:"cycleHistMin"`
	CycleHistMax  float64 `json:"cycleHistMax"`
}
