package domain

import (
	"container/list"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/antitbone/ja4/correlator/internal/observability"
)

const (
	// DefaultMaxHTTPBufferSize is the default maximum number of HTTP events (source A)
	DefaultMaxHTTPBufferSize = 10000
	// DefaultMaxNetworkBufferSize is the default maximum number of network events (source B)
	DefaultMaxNetworkBufferSize = 20000
	// DefaultTimeWindow is used when no valid time window is provided
	// Increased to 10s to support HTTP Keep-Alive scenarios where multiple
	// HTTP requests arrive within a short time window on the same connection.
	DefaultTimeWindow = 10 * time.Second
	// DefaultNetworkTTLS is the default TTL for network events in seconds
	// Increased to 120s to support long-lived HTTP Keep-Alive sessions.
	// The TTL is reset on each correlation (Keep-Alive mode), so the network
	// event stays in buffer as long as the connection is active.
	DefaultNetworkTTLS = 120
	// DefaultApacheEmitDelayMs is the default delay before emitting an orphan A event
	// This allows B events to arrive slightly after A and still correlate
	DefaultApacheEmitDelayMs = 500
	// MatchingModeOneToOne indicates single correlation (consume B after match)
	MatchingModeOneToOne = "one_to_one"
	// MatchingModeOneToMany indicates Keep-Alive mode (B can match multiple A)
	MatchingModeOneToMany = "one_to_many"
)

// CorrelationConfig holds the correlation configuration.
type CorrelationConfig struct {
	TimeWindow        time.Duration
	ApacheAlwaysEmit  bool
	ApacheEmitDelayMs int    // Delay in ms before emitting orphan A (default: 500ms)
	NetworkEmit       bool
	MaxHTTPBufferSize int    // Maximum events to buffer for source A (HTTP)
	MaxNetworkBufferSize int // Maximum events to buffer for source B (Network)
	NetworkTTLS       int    // TTL in seconds for network events (source B)
	MatchingMode      string   // "one_to_one" or "one_to_many" (Keep-Alive)
	ExcludeSourceIPs  []string // List of source IPs or CIDR ranges to exclude
	IncludeDestPorts  []int    // If non-empty, only correlate events matching these destination ports
}

// pendingOrphan represents an A event waiting to be emitted as orphan.
type pendingOrphan struct {
	event     *NormalizedEvent
	emitAfter time.Time // Timestamp when this orphan should be emitted
}

// CorrelationService handles the correlation logic between source A and B events.
type CorrelationService struct {
	config           CorrelationConfig
	mu               sync.Mutex
	bufferA          *eventBuffer
	bufferB          *eventBuffer
	pendingA         map[string][]*list.Element      // key -> ordered elements containing *NormalizedEvent
	pendingB         map[string][]*list.Element
	networkTTLs      map[*list.Element]time.Time     // TTL expiration time for each B event
	pendingOrphans   map[string][]*pendingOrphan     // key -> A events waiting to be emitted as orphans
	keepAliveSeqA    map[string]int                  // key -> request count per Keep-Alive connection (source A)
	timeProvider     TimeProvider
	logger           *observability.Logger
	metrics          *observability.CorrelationMetrics
	excludeIPsParsed []*net.IPNet // Parsed CIDR ranges for efficient lookup
}

type eventBuffer struct {
	events *list.List
}

// newEventBuffer crée un nouveau tampon d'événements vide basé sur une liste doublement chaînée.
func newEventBuffer() *eventBuffer {
	return &eventBuffer{
		events: list.New(),
	}
}

// TimeProvider abstracts time for testability.
type TimeProvider interface {
	Now() time.Time
}

// RealTimeProvider uses real system time.
type RealTimeProvider struct{}

func (p *RealTimeProvider) Now() time.Time {
	return time.Now()
}

// NewCorrelationService creates a new correlation service.
func NewCorrelationService(config CorrelationConfig, timeProvider TimeProvider) *CorrelationService {
	if timeProvider == nil {
		timeProvider = &RealTimeProvider{}
	}
	if config.MaxHTTPBufferSize <= 0 {
		config.MaxHTTPBufferSize = DefaultMaxHTTPBufferSize
	}
	if config.MaxNetworkBufferSize <= 0 {
		config.MaxNetworkBufferSize = DefaultMaxNetworkBufferSize
	}
	if config.TimeWindow <= 0 {
		config.TimeWindow = DefaultTimeWindow
	}
	if config.NetworkTTLS <= 0 {
		config.NetworkTTLS = DefaultNetworkTTLS
	}
	if config.MatchingMode == "" {
		config.MatchingMode = MatchingModeOneToMany // Default to Keep-Alive
	}
	// Zero delay is valid for backward compatibility (immediate emission mode)
	if config.ApacheEmitDelayMs < 0 {
		config.ApacheEmitDelayMs = DefaultApacheEmitDelayMs
	}

	// Parse excluded IPs (CIDR ranges) for efficient lookup
	var excludeIPsParsed []*net.IPNet
	for _, exclude := range config.ExcludeSourceIPs {
		if strings.Contains(exclude, "/") {
			_, cidr, err := net.ParseCIDR(exclude)
			if err == nil {
				excludeIPsParsed = append(excludeIPsParsed, cidr)
			}
		}
	}

	return &CorrelationService{
		config:         config,
		bufferA:        newEventBuffer(),
		bufferB:        newEventBuffer(),
		pendingA:       make(map[string][]*list.Element),
		pendingB:       make(map[string][]*list.Element),
		pendingOrphans: make(map[string][]*pendingOrphan),
		networkTTLs:    make(map[*list.Element]time.Time),
		keepAliveSeqA:  make(map[string]int),
		timeProvider:   timeProvider,
		logger:         observability.NewLogger("correlation"),
		metrics:        observability.NewCorrelationMetrics(),
		excludeIPsParsed: excludeIPsParsed,
	}
}

// SetLogger sets the logger for the correlation service.
func (s *CorrelationService) SetLogger(logger *observability.Logger) {
	s.logger = logger
}

// isIPExcluded checks if a source IP should be excluded from correlation.
func (s *CorrelationService) isIPExcluded(ip string) bool {
	// Check exact IP matches first (from config)
	for _, exclude := range s.config.ExcludeSourceIPs {
		if !strings.Contains(exclude, "/") {
			// Exact IP match
			if exclude == ip {
				return true
			}
			if excludeIP := net.ParseIP(exclude); excludeIP != nil {
				if parsedIP := net.ParseIP(ip); parsedIP != nil {
					if excludeIP.Equal(parsedIP) {
						return true
					}
				}
			}
		}
	}

	// Check CIDR ranges
	parsedIP := net.ParseIP(ip)
	if parsedIP == nil {
		return false
	}

	for _, cidr := range s.excludeIPsParsed {
		if cidr.Contains(parsedIP) {
			return true
		}
	}

	return false
}

// isDestPortFiltered returns true if the event's destination port is NOT in the
// include list. When IncludeDestPorts is empty, all ports are allowed.
func (s *CorrelationService) isDestPortFiltered(port int) bool {
	if len(s.config.IncludeDestPorts) == 0 {
		return false // no filter configured: allow all
	}
	for _, p := range s.config.IncludeDestPorts {
		if p == port {
			return false // port is in the allow-list
		}
	}
	return true // port not in allow-list: filter out
}

// ProcessEvent processes an incoming event and returns correlated logs if matches are found.
func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLog {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Check if source IP is excluded
	if s.isIPExcluded(event.SrcIP) {
		s.logger.Debugf("event excluded by IP filter: source=%s src_ip=%s src_port=%d",
			event.Source, event.SrcIP, event.SrcPort)
		s.metrics.RecordCorrelationFailed("ip_excluded")
		return nil
	}

	// Check if destination port is in the allow-list
	if s.isDestPortFiltered(event.DstPort) {
		s.logger.Debugf("event excluded by dest port filter: source=%s dst_port=%d",
			event.Source, event.DstPort)
		s.metrics.RecordCorrelationFailed("dest_port_filtered")
		// A events (HTTP) are always emitted even when dest port is filtered,
		// so they reach ClickHouse as uncorrelated entries.
		if event.Source == SourceA && s.config.ApacheAlwaysEmit {
			s.metrics.RecordOrphanEmitted("A")
			s.logger.Warnf("orphan A event (dest port filtered): src_ip=%s src_port=%d dst_port=%d",
				event.SrcIP, event.SrcPort, event.DstPort)
			return []CorrelatedLog{NewCorrelatedLogFromEvent(event, "A")}
		}
		return nil
	}

	// Record event received
	s.metrics.RecordEventReceived(string(event.Source))

	// Clean expired events first; collect any orphans forced out by B TTL expiry
	ttlOrphans := s.cleanExpired()

	// Emit pending orphans that have passed their delay
	orphanResults := s.emitPendingOrphans()
	if len(ttlOrphans) > 0 {
		orphanResults = append(ttlOrphans, orphanResults...)
	}

	// Check buffer overflow before adding
	if s.isBufferFull(event.Source) {
		// Buffer full - rotate oldest event instead of dropping new one
		s.logger.Warnf("buffer full, rotating oldest event: source=%s src_ip=%s src_port=%d",
			event.Source, event.SrcIP, event.SrcPort)
		s.metrics.RecordCorrelationFailed("buffer_eviction")
		if event.Source == SourceA {
			// Remove oldest A event and emit as orphan if configured
			if rotated := s.rotateOldestA(); rotated != nil {
				orphanResults = append(orphanResults, *rotated)
			}
		} else if event.Source == SourceB {
			// Remove oldest B event (no emission for B)
			s.rotateOldestB()
		}
		// Continue to add the new event after rotation
	}

	var (
		results      []CorrelatedLog
		shouldBuffer bool
	)

	switch event.Source {
	case SourceA:
		results, shouldBuffer = s.processSourceA(event)
	case SourceB:
		results, shouldBuffer = s.processSourceB(event)
	default:
		s.logger.Warnf("unknown event source: %s", event.Source)
		return nil
	}

	if shouldBuffer {
		s.addEvent(event)
		s.logger.Debugf("event buffered: source=%s src_ip=%s src_port=%d buffer_size=%d",
			event.Source, event.SrcIP, event.SrcPort, s.getBufferSize(event.Source))
	}

	// Update buffer sizes in metrics
	s.metrics.UpdateBufferSizes(int64(s.bufferA.events.Len()), int64(s.bufferB.events.Len()))

	// Combine orphan results with correlation results
	if len(orphanResults) > 0 {
		results = append(orphanResults, results...)
	}

	return results
}

// getBufferSize retourne la taille actuelle du tampon correspondant à la source donnée.
func (s *CorrelationService) getBufferSize(source EventSource) int {
	switch source {
	case SourceA:
		return s.bufferA.events.Len()
	case SourceB:
		return s.bufferB.events.Len()
	}
	return 0
}

// isBufferFull vérifie si le tampon de la source donnée a atteint sa capacité maximale.
func (s *CorrelationService) isBufferFull(source EventSource) bool {
	switch source {
	case SourceA:
		return s.bufferA.events.Len() >= s.config.MaxHTTPBufferSize
	case SourceB:
		return s.bufferB.events.Len() >= s.config.MaxNetworkBufferSize
	}
	return false
}

// rotateOldestA removes the oldest A event from the buffer and returns it as orphan if configured.
func (s *CorrelationService) rotateOldestA() *CorrelatedLog {
	elem := s.bufferA.events.Front()
	if elem == nil {
		return nil
	}

	event := elem.Value.(*NormalizedEvent)
	key := event.CorrelationKey()

	// Remove from buffer
	s.bufferA.events.Remove(elem)
	s.pendingA[key] = removeElementFromSlice(s.pendingA[key], elem)
	if len(s.pendingA[key]) == 0 {
		delete(s.pendingA, key)
	}

	// Emit as orphan if configured
	if s.config.ApacheAlwaysEmit {
		s.logger.Warnf("orphan A event (buffer rotation): src_ip=%s src_port=%d keepalive_seq=%d", event.SrcIP, event.SrcPort, event.KeepAliveSeq)
		s.metrics.RecordOrphanEmitted("A")
		orphan := NewCorrelatedLogFromEvent(event, "A")
		return &orphan
	}
	return nil
}

// rotateOldestB removes the oldest B event from the buffer (no emission).
func (s *CorrelationService) rotateOldestB() {
	elem := s.bufferB.events.Front()
	if elem == nil {
		return
	}

	event := elem.Value.(*NormalizedEvent)
	key := event.CorrelationKey()

	// Remove from buffer
	s.bufferB.events.Remove(elem)
	s.pendingB[key] = removeElementFromSlice(s.pendingB[key], elem)
	if len(s.pendingB[key]) == 0 {
		delete(s.pendingB, key)
	}
	// Remove from TTL map
	delete(s.networkTTLs, elem)
}

// processSourceA traite un événement de source A (HTTP/Apache) et retourne les journaux corrélés ou les place en attente d'orphelins.
func (s *CorrelationService) processSourceA(event *NormalizedEvent) ([]CorrelatedLog, bool) {
	key := event.CorrelationKey()
	// Assign Keep-Alive sequence number (1-based) for this connection
	s.keepAliveSeqA[key]++
	event.KeepAliveSeq = s.keepAliveSeqA[key]
	s.logger.Debugf("processing A event: key=%s keepalive_seq=%d timestamp=%v", key, event.KeepAliveSeq, event.Timestamp)

	// Look for matching B events
	// In one_to_many (Keep-Alive) mode use TTL-based matching: B is valid as long as
	// its TTL has not expired (the timestamp is fixed at TCP connection time and grows
	// beyond TimeWindow after ~10s, making timestamp comparison incorrect).
	matches := s.findMatches(s.bufferB, s.pendingB, key, func(other *NormalizedEvent) bool {
		if s.config.MatchingMode == MatchingModeOneToMany {
			return s.bEventHasValidTTL(other)
		}
		return s.eventsMatch(event, other)
	})

	if len(matches) > 0 {
		var results []CorrelatedLog
		// Correlate with all matching B events (one-to-many)
		for _, bEvent := range matches {
			correlated := NewCorrelatedLog(event, bEvent)
			s.logger.Debugf("correlation found: A(src_ip=%s src_port=%d ts=%v) + B(src_ip=%s src_port=%d ts=%v)",
				event.SrcIP, event.SrcPort, event.Timestamp, bEvent.SrcIP, bEvent.SrcPort, bEvent.Timestamp)
			results = append(results, correlated)
			s.metrics.RecordCorrelationSuccess()

			// Reset TTL for matched B event (Keep-Alive)
			if s.config.MatchingMode == MatchingModeOneToMany {
				// Find the element for this B event and reset TTL
				bKey := bEvent.CorrelationKey()
				if elements, ok := s.pendingB[bKey]; ok {
					for _, elem := range elements {
						if elem.Value.(*NormalizedEvent) == bEvent {
							s.resetNetworkTTL(elem)
							s.metrics.RecordKeepAliveReset()
							s.logger.Debugf("TTL reset for B event (Keep-Alive): key=%s new_ttl=%v",
								bKey, s.config.NetworkTTLS)
							break
						}
					}
				}
			}
		}

		// In one-to-one mode, remove the first matching B
		if s.config.MatchingMode == MatchingModeOneToOne {
			s.removeEvent(s.bufferB, s.pendingB, matches[0])
		}

		return results, false
	}

	// No match found - check if there are B events with same key but outside match criteria
	if bElements, ok := s.pendingB[key]; ok && len(bElements) > 0 {
		if s.config.MatchingMode == MatchingModeOneToMany {
			// In Keep-Alive mode, B exists but TTL expired
			s.logger.Debugf("A event has same key as B but B TTL expired: key=%s", key)
		} else {
			// In one-to-one mode, B exists but outside time window
			for _, bElem := range bElements {
				bEvent := bElem.Value.(*NormalizedEvent)
				timeDiff := event.Timestamp.Sub(bEvent.Timestamp)
				if timeDiff < 0 {
					timeDiff = -timeDiff
				}
				s.logger.Debugf("A event has same key as B but outside time window: key=%s time_diff=%v window=%v",
					key, timeDiff, s.config.TimeWindow)
			}
		}
		s.metrics.RecordCorrelationFailed("time_window")
	} else {
		// No B events with same key at all
		s.logger.Debugf("A event has no matching B key in buffer: key=%s", key)
		s.metrics.RecordCorrelationFailed("no_match_key")
	}

	// No match found - orphan A event
	// Instead of emitting immediately, add to pending orphans with delay
	// This allows B events to arrive slightly after A and still correlate
	if s.config.ApacheAlwaysEmit {
		// Zero delay = immediate emission (backward compatibility mode)
		if s.config.ApacheEmitDelayMs == 0 {
			orphan := NewCorrelatedLogFromEvent(event, "A")
			s.logger.Warnf("orphan A event (immediate): src_ip=%s src_port=%d keepalive_seq=%d", event.SrcIP, event.SrcPort, event.KeepAliveSeq)
			s.metrics.RecordOrphanEmitted("A")
			return []CorrelatedLog{orphan}, false
		}
		s.addPendingOrphan(event)
		s.metrics.RecordPendingOrphan()
		s.logger.Debugf("A event added to pending orphans (delay=%dms): src_ip=%s src_port=%d",
			s.config.ApacheEmitDelayMs, event.SrcIP, event.SrcPort)
		// Don't emit yet - will be emitted after delay expires
		// Return empty results, event stays in pending orphans
		return nil, false
	}

	// Keep in buffer for potential future match
	return nil, true
}

// processSourceB traite un événement de source B (réseau) et retourne les journaux corrélés si une correspondance est trouvée.
func (s *CorrelationService) processSourceB(event *NormalizedEvent) ([]CorrelatedLog, bool) {
	key := event.CorrelationKey()
	s.logger.Debugf("processing B event: key=%s timestamp=%v", key, event.Timestamp)

	// FIRST: Check if there's a pending orphan A that matches this B event
	// This is the key optimization for delayed orphan emission
	if aEvent := s.checkPendingOrphansForCorrelation(event); aEvent != nil {
		correlated := NewCorrelatedLog(aEvent, event)
		s.logger.Debugf("correlation found (pending orphan): A(src_ip=%s src_port=%d ts=%v) + B(src_ip=%s src_port=%d ts=%v)",
			aEvent.SrcIP, aEvent.SrcPort, aEvent.Timestamp, event.SrcIP, event.SrcPort, event.Timestamp)
		s.metrics.RecordCorrelationSuccess()
		s.metrics.RecordPendingOrphanMatch()
		// In one_to_many mode, B must remain in buffer so future A events on the same
		// Keep-Alive connection can also match against it.
		shouldBuffer := s.config.MatchingMode == MatchingModeOneToMany
		return []CorrelatedLog{correlated}, shouldBuffer
	}

	// SECOND: Look for the first matching A event in buffer
	if aEvent := s.findAndPopFirstMatch(s.bufferA, s.pendingA, key, func(other *NormalizedEvent) bool {
		return s.eventsMatch(other, event)
	}); aEvent != nil {
		correlated := NewCorrelatedLog(aEvent, event)
		s.logger.Debugf("correlation found: A(src_ip=%s src_port=%d ts=%v) + B(src_ip=%s src_port=%d ts=%v)",
			aEvent.SrcIP, aEvent.SrcPort, aEvent.Timestamp, event.SrcIP, event.SrcPort, event.Timestamp)
		s.metrics.RecordCorrelationSuccess()
		return []CorrelatedLog{correlated}, false
	}

	// No match found - check if there are A events with same key but outside time window
	if aElements, ok := s.pendingA[key]; ok && len(aElements) > 0 {
		// Key exists but no time match - log the time difference for debugging
		for _, aElem := range aElements {
			aEvent := aElem.Value.(*NormalizedEvent)
			timeDiff := aEvent.Timestamp.Sub(event.Timestamp)
			if timeDiff < 0 {
				timeDiff = -timeDiff
			}
			s.logger.Debugf("B event has same key as A but outside time window: key=%s time_diff=%v window=%v",
				key, timeDiff, s.config.TimeWindow)
		}
		s.metrics.RecordCorrelationFailed("time_window")
	} else {
		// No A events with same key at all
		s.logger.Debugf("B event has no matching A key in buffer: key=%s", key)
		s.metrics.RecordCorrelationFailed("no_match_key")
	}

	// Never emit B alone. Keep in buffer for potential future match.
	s.logger.Debugf("B event buffered (no match yet): key=%s src_ip=%s src_port=%d",
		key, event.SrcIP, event.SrcPort)
	return nil, true
}

// eventsMatch vérifie si deux événements se trouvent dans la fenêtre temporelle de corrélation configurée.
func (s *CorrelationService) eventsMatch(a, b *NormalizedEvent) bool {
	diff := a.Timestamp.Sub(b.Timestamp)
	if diff < 0 {
		diff = -diff
	}
	return diff <= s.config.TimeWindow
}

// bEventHasValidTTL returns true if the B event is still alive (TTL not expired).
// Used in one_to_many mode where the B timestamp is fixed at TCP connection time and
// grows beyond TimeWindow — TTL validity is the correct liveness indicator.
func (s *CorrelationService) bEventHasValidTTL(bEvent *NormalizedEvent) bool {
	key := bEvent.CorrelationKey()
	if elements, ok := s.pendingB[key]; ok {
		for _, elem := range elements {
			if elem.Value.(*NormalizedEvent) == bEvent {
				if ttl, exists := s.networkTTLs[elem]; exists {
					return !s.timeProvider.Now().After(ttl)
				}
			}
		}
	}
	return false
}

// addEvent ajoute un événement au tampon correspondant à sa source et initialise son TTL réseau si nécessaire.
func (s *CorrelationService) addEvent(event *NormalizedEvent) {
	key := event.CorrelationKey()

	switch event.Source {
	case SourceA:
		elem := s.bufferA.events.PushBack(event)
		s.pendingA[key] = append(s.pendingA[key], elem)
	case SourceB:
		elem := s.bufferB.events.PushBack(event)
		s.pendingB[key] = append(s.pendingB[key], elem)
		// Set TTL for network event
		s.networkTTLs[elem] = s.timeProvider.Now().Add(time.Duration(s.config.NetworkTTLS) * time.Second)
	}
}

// cleanExpired supprime les événements expirés des tampons et retourne les orphelins forcés par l'expiration du TTL réseau.
func (s *CorrelationService) cleanExpired() []CorrelatedLog {
	// Clean expired B events first - use TTL map only (not event timestamp)
	// This is critical for Keep-Alive: TTL is reset on each correlation,
	// so we must check the reset TTL, not the original event timestamp.
	// Returns any orphan A events that must be emitted because their B expired.
	orphans := s.cleanNetworkBufferByTTL()

	// Clean A events based on B TTL, not system time
	// An A event should only be removed if:
	// 1. It has already been correlated (no longer in buffer), OR
	// 2. All potential matching B events have expired (TTL exceeded)
	s.cleanBufferAByBTTL()

	return orphans
}

// cleanBufferAByBTTL cleans A events based on whether corresponding B events still exist.
// An A event is removed only if:
// 1. All potential matching B events have expired (TTL exceeded), OR
// 2. The A event itself is too old (beyond TimeWindow from current time)
// When removed, the A event is emitted as orphan if ApacheAlwaysEmit=true.
func (s *CorrelationService) cleanBufferAByBTTL() {
	now := s.timeProvider.Now()
	var toRemove []*list.Element

	// First pass: identify A events to remove
	for elem := s.bufferA.events.Front(); elem != nil; elem = elem.Next() {
		event := elem.Value.(*NormalizedEvent)
		key := event.CorrelationKey()

		// Check if there's any non-expired B event with the same key
		hasValidB := false
		if bElements, ok := s.pendingB[key]; ok {
			for _, bElem := range bElements {
				if ttl, exists := s.networkTTLs[bElem]; exists {
					if !now.After(ttl) {
						hasValidB = true
						break
					}
				}
			}
		}

		// Remove A if no valid B exists AND A is beyond TimeWindow from now
		if !hasValidB {
			aAge := now.Sub(event.Timestamp)
			if aAge > s.config.TimeWindow {
				toRemove = append(toRemove, elem)
			}
		}
	}

	// Second pass: remove identified events and emit as orphans if configured
	for _, elem := range toRemove {
		event := elem.Value.(*NormalizedEvent)
		key := event.CorrelationKey()
		s.bufferA.events.Remove(elem)
		s.pendingA[key] = removeElementFromSlice(s.pendingA[key], elem)
		if len(s.pendingA[key]) == 0 {
			delete(s.pendingA, key)
		}

		if s.config.ApacheAlwaysEmit {
			s.logger.Warnf("orphan A event (no B match, TTL expired): src_ip=%s src_port=%d key=%s keepalive_seq=%d",
				event.SrcIP, event.SrcPort, key, event.KeepAliveSeq)
			s.metrics.RecordOrphanEmitted("A")
		} else {
			s.logger.Debugf("A event removed (no valid B, TTL expired): src_ip=%s src_port=%d key=%s",
				event.SrcIP, event.SrcPort, key)
		}
	}
}

// cleanBuffer removes expired events from buffer A (based on event timestamp).
// Deprecated: Use cleanBufferAByBTTL for source A events to properly handle
// correlation with B events.
func (s *CorrelationService) cleanBuffer(buffer *eventBuffer, pending map[string][]*list.Element, cutoff time.Time) {
	for elem := buffer.events.Front(); elem != nil; {
		next := elem.Next()
		event := elem.Value.(*NormalizedEvent)

		if event.Timestamp.Before(cutoff) {
			key := event.CorrelationKey()
			buffer.events.Remove(elem)
			pending[key] = removeElementFromSlice(pending[key], elem)
			if len(pending[key]) == 0 {
				delete(pending, key)
			}
		}
		elem = next
	}
}

// cleanNetworkBufferByTTL removes expired B events based on their reset TTL.
// For Keep-Alive support, we check the TTL map (which is reset on each correlation)
// rather than the original event timestamp.
// When a B event expires, any pending orphan A events for that key are immediately
// returned for emission (they can no longer be correlated — their B connection is gone).
func (s *CorrelationService) cleanNetworkBufferByTTL() []CorrelatedLog {
	now := s.timeProvider.Now()
	var removed int
	var forced []CorrelatedLog
	for elem, ttl := range s.networkTTLs {
		if now.After(ttl) {
			event := elem.Value.(*NormalizedEvent)
			key := event.CorrelationKey()
			s.bufferB.events.Remove(elem)
			s.pendingB[key] = removeElementFromSlice(s.pendingB[key], elem)
			if len(s.pendingB[key]) == 0 {
				delete(s.pendingB, key)
				// Connection fully gone: reset Keep-Alive counter for this key
				delete(s.keepAliveSeqA, key)

				// Immediately emit any pending orphan A events for this key.
				// They can no longer correlate with a B (the connection is closed),
				// and waiting for the timer would delay them unnecessarily.
				if s.config.ApacheAlwaysEmit {
					if orphans, ok := s.pendingOrphans[key]; ok && len(orphans) > 0 {
						s.logger.Debugf("B TTL expired, emitting %d pending orphan(s) for key=%s", len(orphans), key)
						for _, o := range orphans {
							s.logger.Warnf("orphan A event (B TTL expired): src_ip=%s src_port=%d key=%s keepalive_seq=%d",
								o.event.SrcIP, o.event.SrcPort, key, o.event.KeepAliveSeq)
							s.metrics.RecordOrphanEmitted("A")
							forced = append(forced, NewCorrelatedLogFromEvent(o.event, "A"))
						}
						delete(s.pendingOrphans, key)
					}
				}
			}
			delete(s.networkTTLs, elem)
			removed++
			s.logger.Debugf("B event TTL expired: key=%s src_ip=%s src_port=%d ttl=%v age=%v",
				key, event.SrcIP, event.SrcPort, s.config.NetworkTTLS, now.Sub(event.Timestamp))
			s.metrics.RecordCorrelationFailed("ttl_expired")
		}
	}
	if removed > 0 {
		s.logger.Debugf("cleaned %d expired B events", removed)
	}
	return forced
}

// findAndPopFirstMatch recherche et supprime le premier événement satisfaisant le critère dans le tampon.
func (s *CorrelationService) findAndPopFirstMatch(
	buffer *eventBuffer,
	pending map[string][]*list.Element,
	key string,
	matcher func(*NormalizedEvent) bool,
) *NormalizedEvent {
	elements, ok := pending[key]
	if !ok || len(elements) == 0 {
		return nil
	}

	for idx, elem := range elements {
		other := elem.Value.(*NormalizedEvent)
		if !matcher(other) {
			continue
		}

		buffer.events.Remove(elem)
		updated := append(elements[:idx], elements[idx+1:]...)
		if len(updated) == 0 {
			delete(pending, key)
		} else {
			pending[key] = updated
		}

		return other
	}

	return nil
}

// findMatches returns all matching events without removing them (for one-to-many).
func (s *CorrelationService) findMatches(
	buffer *eventBuffer,
	pending map[string][]*list.Element,
	key string,
	matcher func(*NormalizedEvent) bool,
) []*NormalizedEvent {
	elements, ok := pending[key]
	if !ok || len(elements) == 0 {
		return nil
	}

	var matches []*NormalizedEvent
	for _, elem := range elements {
		other := elem.Value.(*NormalizedEvent)
		if matcher(other) {
			matches = append(matches, other)
		}
	}

	return matches
}

// getElementByKey finds the list element for a given event in pending map.
func (s *CorrelationService) getElementByKey(pending map[string][]*list.Element, key string, event *NormalizedEvent) *list.Element {
	elements, ok := pending[key]
	if !ok {
		return nil
	}

	for _, elem := range elements {
		if elem.Value.(*NormalizedEvent) == event {
			return elem
		}
	}
	return nil
}

// removeEvent removes an event from buffer and pending maps.
func (s *CorrelationService) removeEvent(buffer *eventBuffer, pending map[string][]*list.Element, event *NormalizedEvent) {
	key := event.CorrelationKey()
	elements, ok := pending[key]
	if !ok {
		return
	}

	for idx, elem := range elements {
		if elem.Value.(*NormalizedEvent) == event {
			buffer.events.Remove(elem)
			updated := append(elements[:idx], elements[idx+1:]...)
			if len(updated) == 0 {
				delete(pending, key)
			} else {
				pending[key] = updated
			}
			// Remove from TTL map if present
			delete(s.networkTTLs, elem)
			break
		}
	}
}

// resetNetworkTTL resets the TTL for a network event (Keep-Alive).
func (s *CorrelationService) resetNetworkTTL(elem *list.Element) {
	if elem == nil {
		return
	}
	s.networkTTLs[elem] = s.timeProvider.Now().Add(time.Duration(s.config.NetworkTTLS) * time.Second)
}

// addPendingOrphan adds an A event to the pending orphans list with a delayed emission.
func (s *CorrelationService) addPendingOrphan(event *NormalizedEvent) {
	key := event.CorrelationKey()
	emitAfter := s.timeProvider.Now().Add(time.Duration(s.config.ApacheEmitDelayMs) * time.Millisecond)
	
	orphan := &pendingOrphan{
		event:     event,
		emitAfter: emitAfter,
	}
	
	s.pendingOrphans[key] = append(s.pendingOrphans[key], orphan)
	s.logger.Debugf("A event added to pending orphans: src_ip=%s src_port=%d emit_after=%v",
		event.SrcIP, event.SrcPort, emitAfter)
}

// removePendingOrphan removes a specific pending orphan by event reference.
// Returns true if found and removed, false otherwise.
func (s *CorrelationService) removePendingOrphan(event *NormalizedEvent) bool {
	key := event.CorrelationKey()
	orphans, ok := s.pendingOrphans[key]
	if !ok {
		return false
	}
	
	for i, orphan := range orphans {
		if orphan.event == event {
			s.pendingOrphans[key] = append(orphans[:i], orphans[i+1:]...)
			if len(s.pendingOrphans[key]) == 0 {
				delete(s.pendingOrphans, key)
			}
			return true
		}
	}
	return false
}

// checkPendingOrphansForCorrelation checks if any pending orphans match the given B event.
// Returns the first matching A event and removes it from pending orphans.
func (s *CorrelationService) checkPendingOrphansForCorrelation(bEvent *NormalizedEvent) *NormalizedEvent {
	key := bEvent.CorrelationKey()
	orphans, ok := s.pendingOrphans[key]
	if !ok || len(orphans) == 0 {
		return nil
	}
	
	for i, orphan := range orphans {
		// In one_to_many mode: same key = same Keep-Alive connection → always correlate.
		// The incoming B event may have an old original timestamp (fixed at TCP connection
		// time) so eventsMatch would fail for long-lived sessions. Use key identity instead.
		var matched bool
		if s.config.MatchingMode == MatchingModeOneToMany {
			matched = true
		} else {
			matched = s.eventsMatch(orphan.event, bEvent)
		}
		if matched {
			// Found a match! Remove from pending and return
			aEvent := orphan.event
			s.pendingOrphans[key] = append(orphans[:i], orphans[i+1:]...)
			if len(s.pendingOrphans[key]) == 0 {
				delete(s.pendingOrphans, key)
			}
			s.logger.Debugf("pending orphan matched with B: A(src_ip=%s src_port=%d) + B(src_ip=%s src_port=%d)",
				aEvent.SrcIP, aEvent.SrcPort, bEvent.SrcIP, bEvent.SrcPort)
			return aEvent
		}
	}
	
	return nil
}

// emitPendingOrphans emits all pending orphans that have passed their emit delay.
// Returns the list of correlated logs to emit.
func (s *CorrelationService) emitPendingOrphans() []CorrelatedLog {
	if !s.config.ApacheAlwaysEmit {
		return nil
	}

	now := s.timeProvider.Now()
	var results []CorrelatedLog

	// Iterate over keys only (not values) to avoid stale slice aliasing after mutations.
	for key := range s.pendingOrphans {
		var remaining []*pendingOrphan
		for _, o := range s.pendingOrphans[key] {
			if now.After(o.emitAfter) {
				orphan := NewCorrelatedLogFromEvent(o.event, "A")
				s.logger.Warnf("orphan A event (emit delay expired): src_ip=%s src_port=%d key=%s keepalive_seq=%d delay_ms=%d",
					o.event.SrcIP, o.event.SrcPort, key, o.event.KeepAliveSeq, s.config.ApacheEmitDelayMs)
				s.metrics.RecordOrphanEmitted("A")
				results = append(results, orphan)
			} else {
				remaining = append(remaining, o)
			}
		}
		if len(remaining) == 0 {
			delete(s.pendingOrphans, key)
		} else {
			s.pendingOrphans[key] = remaining
		}
	}

	return results
}

// EmitPendingOrphans emits all pending orphans that have passed their emit delay.
// This is the public, thread-safe version intended to be called by an external ticker
// (e.g. the Orchestrator) so that orphans are emitted even when no new events arrive.
func (s *CorrelationService) EmitPendingOrphans() []CorrelatedLog {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.emitPendingOrphans()
}

// removeElementFromSlice retire l'élément ciblé d'une tranche de list.Element sans modifier l'ordre.
func removeElementFromSlice(elements []*list.Element, target *list.Element) []*list.Element {
	if len(elements) == 0 {
		return elements
	}
	for i, elem := range elements {
		if elem == target {
			return append(elements[:i], elements[i+1:]...)
		}
	}
	return elements
}

// Flush forces emission of remaining buffered events (for shutdown).
// It first attempts to correlate remaining A and B events, then emits orphans.
func (s *CorrelationService) Flush() []CorrelatedLog {
	s.mu.Lock()
	defer s.mu.Unlock()

	var results []CorrelatedLog

	// First, try to correlate remaining A events with B events
	// This handles the case where both A and B are still in buffer at shutdown
	for elem := s.bufferA.events.Front(); elem != nil; {
		next := elem.Next()
		event := elem.Value.(*NormalizedEvent)
		key := event.CorrelationKey()

		// Look for matching B events
		matched := false
		if bElements, ok := s.pendingB[key]; ok {
			for _, bElem := range bElements {
				bEvent := bElem.Value.(*NormalizedEvent)
				if s.eventsMatch(event, bEvent) {
					// Correlate A with B
					correlated := NewCorrelatedLog(event, bEvent)
					s.logger.Debugf("flush correlation: A(src_ip=%s src_port=%d) + B(src_ip=%s src_port=%d)",
						event.SrcIP, event.SrcPort, bEvent.SrcIP, bEvent.SrcPort)
					results = append(results, correlated)
					matched = true
					break
				}
			}
		}

		if matched {
			s.bufferA.events.Remove(elem)
			s.pendingA[key] = removeElementFromSlice(s.pendingA[key], elem)
			if len(s.pendingA[key]) == 0 {
				delete(s.pendingA, key)
			}
		}
		elem = next
	}

	// Emit remaining A events as orphans if configured
	if s.config.ApacheAlwaysEmit {
		for elem := s.bufferA.events.Front(); elem != nil; elem = elem.Next() {
			event := elem.Value.(*NormalizedEvent)
			orphan := NewCorrelatedLogFromEvent(event, "A")
			s.logger.Warnf("flush orphan A: src_ip=%s src_port=%d", event.SrcIP, event.SrcPort)
			results = append(results, orphan)
		}
	}

	// Emit all pending orphans (immediately, ignoring delay)
	if s.config.ApacheAlwaysEmit {
		for _, orphans := range s.pendingOrphans {
			for _, orphan := range orphans {
				correlatedLog := NewCorrelatedLogFromEvent(orphan.event, "A")
				s.logger.Warnf("flush pending orphan: src_ip=%s src_port=%d",
					orphan.event.SrcIP, orphan.event.SrcPort)
				results = append(results, correlatedLog)
			}
		}
	}

	// Never emit remaining B events alone.

	// Clear buffers
	s.bufferA.events.Init()
	s.bufferB.events.Init()
	s.pendingA = make(map[string][]*list.Element)
	s.pendingB = make(map[string][]*list.Element)
	s.pendingOrphans = make(map[string][]*pendingOrphan)
	s.networkTTLs = make(map[*list.Element]time.Time)

	return results
}

// GetBufferSizes returns the current buffer sizes (for monitoring).
func (s *CorrelationService) GetBufferSizes() (int, int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.bufferA.events.Len(), s.bufferB.events.Len()
}

// GetMetrics returns the correlation metrics (for monitoring/debugging).
func (s *CorrelationService) GetMetrics() *observability.CorrelationMetrics {
	return s.metrics
}

// GetMetricsSnapshot returns a point-in-time snapshot of metrics.
func (s *CorrelationService) GetMetricsSnapshot() observability.MetricsSnapshot {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.metrics.Snapshot()
}
