package server

import (
	"fmt"
	"math"
	"net"
	"sync"
	"time"
)

// liveNetWindow is how far back the "last minute" byte totals reach.
const liveNetWindow = time.Minute

// liveNetMaxGap bounds how far apart two samples may be before a rate computed
// across them stops being "live". The overview SSE ticks every two seconds, so
// a gap beyond this means the tab was closed or the device was idle; treat it
// as a fresh baseline instead of averaging a long dead interval.
const liveNetMaxGap = 15 * time.Second

// netIfSample is one cumulative counter reading for an interface.
type netIfSample struct {
	at    time.Time
	rxCum uint64
	txCum uint64
}

// liveNetDevice holds the per-device sampling state used to derive rates and
// trailing-window totals from cumulative interface counters.
type liveNetDevice struct {
	prev    netIfSample
	hasPrev bool
	window  []netIfSample
}

// liveNetResult is one rendered snapshot of a device's live network state.
type liveNetResult struct {
	ipv4     string
	rxRate   float64 // bytes/sec over the trailing sample interval
	txRate   float64
	minuteRx int64 // bytes over the trailing liveNetWindow
	minuteTx int64
	status   string // "", "waiting_sample", or "stale"
}

// liveNetTracker derives live rates and last-minute totals from cumulative
// /sys interface counters. It is driven on demand by the overview builders, so
// no separate goroutine is required; the SSE overview cadence keeps it warm.
type liveNetTracker struct {
	mu      sync.Mutex
	devices map[string]*liveNetDevice
}

func newLiveNetTracker() *liveNetTracker {
	return &liveNetTracker{devices: map[string]*liveNetDevice{}}
}

// sample reads the interface's current counters and addresses and returns the
// device's live network state. Interface addresses resolve even on the first
// call; rates and totals need a second reading, reported as waiting_sample.
func (t *liveNetTracker) sample(deviceID, iface string, now time.Time) liveNetResult {
	ipv4 := netIfAddrs(iface)
	rxCum, txCum, err := netIfCounters(iface)
	if err != nil {
		// The interface briefly disappears while QMI reconnects. Drop the
		// baseline so the next good read starts fresh rather than counting the
		// reconnect as one giant delta.
		t.mu.Lock()
		delete(t.devices, deviceID)
		t.mu.Unlock()
		return liveNetResult{ipv4: ipv4, status: "stale"}
	}
	rxRate, txRate, minuteRx, minuteTx, status := t.record(deviceID, rxCum, txCum, now)
	return liveNetResult{
		ipv4: ipv4,
		rxRate: rxRate, txRate: txRate,
		minuteRx: minuteRx, minuteTx: minuteTx,
		status: status,
	}
}

// record folds one cumulative counter reading into the device's sampling state
// and returns the derived rates and trailing-window totals. It is pure (no
// interface I/O) so the rate/window logic is unit-testable.
func (t *liveNetTracker) record(deviceID string, rxCum, txCum uint64, now time.Time) (rxRate, txRate float64, minuteRx, minuteTx int64, status string) {
	t.mu.Lock()
	defer t.mu.Unlock()

	d := t.devices[deviceID]
	if d == nil {
		d = &liveNetDevice{}
		t.devices[deviceID] = d
	}
	current := netIfSample{at: now, rxCum: rxCum, txCum: txCum}

	// First sighting, a counter reset (interface reconnected), or a gap too
	// long to average honestly: establish a baseline and wait for the next
	// reading before reporting a rate.
	if !d.hasPrev || rxCum < d.prev.rxCum || txCum < d.prev.txCum || now.Sub(d.prev.at) > liveNetMaxGap {
		d.prev = current
		d.hasPrev = true
		d.window = []netIfSample{current}
		return 0, 0, 0, 0, "waiting_sample"
	}

	if elapsed := now.Sub(d.prev.at).Seconds(); elapsed > 0 {
		rxRate = float64(rxCum-d.prev.rxCum) / elapsed
		txRate = float64(txCum-d.prev.txCum) / elapsed
	}
	d.prev = current
	d.window = append(d.window, current)

	// Drop samples outside the trailing window, then measure totals against
	// the oldest surviving reading.
	cutoff := now.Add(-liveNetWindow)
	kept := d.window[:0]
	for _, s := range d.window {
		if !s.at.Before(cutoff) {
			kept = append(kept, s)
		}
	}
	d.window = kept
	minuteRx = int64(rxCum - d.window[0].rxCum)
	minuteTx = int64(txCum - d.window[0].txCum)
	return rxRate, txRate, minuteRx, minuteTx, ""
}

// netIfAddrs returns the interface's first global IPv4 address. It uses only
// the net package, so it compiles on every platform; on hosts without the
// interface it returns an empty string.
func netIfAddrs(iface string) (ipv4 string) {
	if iface == "" {
		return ""
	}
	netIf, err := net.InterfaceByName(iface)
	if err != nil {
		return ""
	}
	addrs, err := netIf.Addrs()
	if err != nil {
		return ""
	}
	for _, addr := range addrs {
		var ip net.IP
		switch a := addr.(type) {
		case *net.IPNet:
			ip = a.IP
		case *net.IPAddr:
			ip = a.IP
		}
		if ip == nil || ip.IsLoopback() {
			continue
		}
		if v4 := ip.To4(); v4 != nil {
			return v4.String()
		}
	}
	return ""
}

// formatLiveBytes mirrors the SPA's formatBytes so the live strings match the
// chart's formatting: 1024-based units, rounded once the value reaches 100.
func formatLiveBytes(value float64) string {
	if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
		value = 0
	}
	units := []string{"B", "KB", "MB", "GB", "TB"}
	size := value
	unit := 0
	for size >= 1024 && unit < len(units)-1 {
		size /= 1024
		unit++
	}
	if size >= 100 {
		return fmt.Sprintf("%.0f %s", size, units[unit])
	}
	return fmt.Sprintf("%.1f %s", size, units[unit])
}
