package main

import (
	"time"

	"github.com/netdata/netdata/go/plugins/pkg/netdataapi"
	"github.com/netdata/netdata/src/collectors/ebpf.plugin/ebpfgo.plugin/libbpfloader"
)

// socketGlobalPublish holds the per-cycle delta values for the network-protocols function.
//
// Field names match their semantic direction (Received vs Sent) so the
// BPF-snapshot -> publish and publish -> JSON paths are straight-line code
// with no name swap and no sign trick. The BPF counter -> Received/Sent
// mapping is pinned by TestBuildNetworkProtocolsJSON_BPFMapping.
type socketGlobalPublish struct {
	// TCP received path: tcp_cleanup_rbuf is invoked when the kernel cleans
	// the receive buffer after an application read.
	tcpDimReceivedCalls uint64 // delta of CallsTCPCleanupRbuf
	tcpDimReceivedErr   uint64 // delta of ErrorTCPCleanupRbuf

	// TCP sent path: tcp_sendmsg is invoked when an application sends data.
	tcpDimSentCalls uint64 // delta of CallsTCPSendmsg
	tcpDimSentErr   uint64 // delta of ErrorTCPSendmsg

	tcpCloseCalls uint64 // delta of CallsTCPClose
	tcpRetransmit uint64 // delta of TCPRetransmit
	tcpV4Conn     uint64 // delta of CallsTCPConnectIPv4
	tcpV6Conn     uint64 // delta of CallsTCPConnectIPv6
	udpRecvCalls  uint64 // delta of CallsUDPRecvmsg
	udpSendCalls  uint64 // delta of CallsUDPSendmsg
	udpRecvErr    uint64 // delta of ErrorUDPRecvmsg
	udpSendErr    uint64 // delta of ErrorUDPSendmsg
	inboundTCP    uint64 // delta of InboundConnTCP
	inboundUDP    uint64 // delta of InboundConnUDP

	tcpBytesSent     uint64 // delta of BytesTCPSendmsg
	tcpBytesReceived uint64 // delta of BytesTCPCleanupRbuf
	udpBytesSent     uint64 // delta of BytesUDPSendmsg
	udpBytesReceived uint64 // delta of BytesUDPRecvmsg
}

type socketGlobalState struct {
	initialized bool
	prev        libbpfloader.SocketSnapshot
}

// socketDelta returns the increment of a monotonically increasing BPF counter.
// Returns 0 when prev is zero (first read — avoids a spike from accumulated
// counter history) or when current is not greater than prev (counter reset or wrap).
func socketDelta(current, prev uint64) uint64 {
	if current <= prev {
		return 0
	}
	return current - prev
}

func (s *socketGlobalState) Update(snap libbpfloader.SocketSnapshot) (socketGlobalPublish, bool) {
	if !s.initialized {
		s.prev = snap
		s.initialized = true
		return socketGlobalPublish{}, true
	}

	prev := s.prev
	s.prev = snap

	// BPF counter -> semantic direction is straight: sendmsg = sent,
	// cleanup_rbuf = received. No name swap, no sign trick.
	return socketGlobalPublish{
		tcpDimReceivedCalls: socketDelta(snap.CallsTCPCleanupRbuf, prev.CallsTCPCleanupRbuf),
		tcpDimReceivedErr:   socketDelta(snap.ErrorTCPCleanupRbuf, prev.ErrorTCPCleanupRbuf),
		tcpDimSentCalls:     socketDelta(snap.CallsTCPSendmsg, prev.CallsTCPSendmsg),
		tcpDimSentErr:       socketDelta(snap.ErrorTCPSendmsg, prev.ErrorTCPSendmsg),
		tcpCloseCalls:       socketDelta(snap.CallsTCPClose, prev.CallsTCPClose),
		tcpRetransmit:       socketDelta(snap.TCPRetransmit, prev.TCPRetransmit),
		tcpV4Conn:           socketDelta(snap.CallsTCPConnectIPv4, prev.CallsTCPConnectIPv4),
		tcpV6Conn:           socketDelta(snap.CallsTCPConnectIPv6, prev.CallsTCPConnectIPv6),
		udpRecvCalls:        socketDelta(snap.CallsUDPRecvmsg, prev.CallsUDPRecvmsg),
		udpSendCalls:        socketDelta(snap.CallsUDPSendmsg, prev.CallsUDPSendmsg),
		udpRecvErr:          socketDelta(snap.ErrorUDPRecvmsg, prev.ErrorUDPRecvmsg),
		udpSendErr:          socketDelta(snap.ErrorUDPSendmsg, prev.ErrorUDPSendmsg),
		inboundTCP:          socketDelta(snap.InboundConnTCP, prev.InboundConnTCP),
		inboundUDP:          socketDelta(snap.InboundConnUDP, prev.InboundConnUDP),
		tcpBytesSent:        socketDelta(snap.BytesTCPSendmsg, prev.BytesTCPSendmsg),
		tcpBytesReceived:    socketDelta(snap.BytesTCPCleanupRbuf, prev.BytesTCPCleanupRbuf),
		udpBytesSent:        socketDelta(snap.BytesUDPSendmsg, prev.BytesUDPSendmsg),
		udpBytesReceived:    socketDelta(snap.BytesUDPRecvmsg, prev.BytesUDPRecvmsg),
	}, true
}

// ---- rate-limited stderr ---------------------------------------------------
// (rateLimitedStderr and logPluginErr live in error_log.go; both socket and
// dns collectors use them to throttle stderr noise from persistent failures.)

// ---- collection loop -------------------------------------------------------

// runSocketGlobalCollector is the socket plugin collection loop.
// It runs in its own goroutine alongside cachestat.
// store may be nil when there are no apps/cgroups consumers for socket data.
// shouldPublish must be true when cachestat is not publishing to SHM; in that
// case the socket collector opens the shared segment and publishes each cycle
// so that cgroup.plugin can read socket data independently of cachestat.
func runSocketGlobalCollector(api *netdataapi.API, handle *SocketLegacyHandle, stop <-chan struct{}, updateEvery int, store *cachestatSharedMemoryStore, fnStore *socketFunctionStore, shouldPublish bool) {
	if handle == nil || handle.Runtime == nil {
		return
	}

	if updateEvery <= 0 {
		updateEvery = socketDefaultUpdateEvery
	}

	if store != nil {
		// Clear the SOCKET SHM flag when this goroutine exits so the C consumer
		// stops gating on socket_ok after the module shuts down.
		defer store.MarkSocketInactive()
	}

	state := &socketGlobalState{}

	// SHM publisher: opened lazily inside collectAndPublish so that a transient
	// EEXIST backoff on the first cycle does not permanently prevent publishing.
	// Matches the lazy-open pattern used by cachestat_global.go.
	var shmPublisher *SharedPidMemoryPublisher
	defer func() {
		if shmPublisher != nil {
			shmPublisher.Close()
		}
	}()

	clearSocketApps := func() {
		if store == nil {
			return
		}

		// ORDERING REQUIREMENT: callers MUST call store.MarkSocketInactive()
		// before calling clearSocketApps.  Publish() reads activeModules to
		// stamp the SHM header flags; the SOCKET bit must already be cleared
		// before Publish runs, or the consumer will see socket_ok = true
		// alongside zero data arrays.  UpdateSocketApps(nil) does not set the
		// SOCKET bit (nil signals a failed cycle), so the bit stays cleared.
		store.UpdateSocketApps(nil, 0)
		if shmPublisher != nil {
			if err := store.Publish(shmPublisher); err != nil {
				logPluginErr("socket.publish", "socket", "shared memory publish", err)
			}
		}
	}

	collectAndPublish := func(usecSince int) {
		if shmPublisher == nil && shouldPublish && store != nil {
			p, perr := NewSharedPidMemoryPublisher(productionSHMName, productionSEMName, handle.PidTableSize, uint32(updateEvery))
			if perr != nil {
				logPluginErr("socket.shm_open", "socket", "shared memory open", perr)
			} else {
				shmPublisher = p
			}
		}

		snap, err := handle.Runtime.Snapshot(handle.MapsPerCore)
		if err != nil {
			logPluginErr("socket.snapshot", "socket", "snapshot", err)
			if store != nil {
				// No valid data this cycle: clear the SOCKET flag so the
				// consumer does not see socket_ok = true with zero arrays.
				store.MarkSocketInactive()
			}
			clearSocketApps()
			return
		}
		if publish, ok := state.Update(snap); ok {
			fnStore.update(publish)
		}

		if store != nil {
			pidEntries, pidErr := handle.Runtime.SnapshotPerPID()
			if pidErr != nil {
				logPluginErr("socket.snapshot_per_pid", "socket", "per-PID snapshot", pidErr)
				// Global snapshot succeeded but per-PID data is missing.
				// Clear the flag so the consumer skips this cycle's update.
				store.MarkSocketInactive()
				clearSocketApps()
			} else {
				store.UpdateSocketApps(pidEntries, uint32(updateEvery))
				if shmPublisher != nil {
					if err := store.Publish(shmPublisher); err != nil {
						logPluginErr("socket.publish", "socket", "shared memory publish", err)
					}
				}
			}
		}
	}

	collectAndPublish(0)
	lastCollection := time.Now()

	ticker := time.NewTicker(time.Duration(updateEvery) * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-stop:
			return
		case <-ticker.C:
		}

		now := time.Now()
		usecSince := max(int(now.Sub(lastCollection).Microseconds()), 0)
		lastCollection = now
		collectAndPublish(usecSince)
	}
}
