package domain

import (
	"encoding/json"
	"reflect"
	"time"
)

// CorrelatedLog represents the output correlated log entry.
// All fields are flattened into a single-level structure.
type CorrelatedLog struct {
	Timestamp  time.Time      `json:"timestamp"`
	SrcIP      string         `json:"src_ip"`
	SrcPort    int            `json:"src_port"`
	DstIP      string         `json:"dst_ip,omitempty"`
	DstPort    int            `json:"dst_port,omitempty"`
	Correlated int            `json:"correlated"` // 0 = orphelin, 1 = corrélé
	OrphanSide string         `json:"orphan_side,omitempty"`
	Fields     map[string]any `json:"-"` // Additional fields, merged at marshal time
}

// MarshalJSON implements custom JSON marshaling to flatten the structure.
func (c CorrelatedLog) MarshalJSON() ([]byte, error) {
	// Create a flat map with all fields
	flat := make(map[string]any)

	// Add core fields
	flat["timestamp"] = c.Timestamp
	flat["src_ip"] = c.SrcIP
	flat["src_port"] = c.SrcPort
	if c.DstIP != "" {
		flat["dst_ip"] = c.DstIP
	}
	if c.DstPort != 0 {
		flat["dst_port"] = c.DstPort
	}
	flat["correlated"] = c.Correlated
	if c.OrphanSide != "" {
		flat["orphan_side"] = c.OrphanSide
	}

	// Merge additional fields while preserving reserved keys
	reservedKeys := map[string]struct{}{
		"timestamp":   {},
		"src_ip":      {},
		"src_port":    {},
		"dst_ip":      {},
		"dst_port":    {},
		"correlated":  {},
		"orphan_side": {},
	}
	for k, v := range c.Fields {
		if _, reserved := reservedKeys[k]; reserved {
			continue
		}
		flat[k] = v
	}

	return json.Marshal(flat)
}

// NewCorrelatedLogFromEvent creates a correlated log from a single event (orphan).
func NewCorrelatedLogFromEvent(event *NormalizedEvent, orphanSide string) CorrelatedLog {
	fields := extractFields(event)
	if event.KeepAliveSeq > 0 {
		fields["keepalives"] = event.KeepAliveSeq
	}
	return CorrelatedLog{
		Timestamp:  event.Timestamp,
		SrcIP:      event.SrcIP,
		SrcPort:    event.SrcPort,
		DstIP:      event.DstIP,
		DstPort:    event.DstPort,
		Correlated: 0,
		OrphanSide: orphanSide,
		Fields:     fields,
	}
}

// NewCorrelatedLog creates a correlated log from two matched events.
func NewCorrelatedLog(apacheEvent, networkEvent *NormalizedEvent) CorrelatedLog {
	ts := apacheEvent.Timestamp
	if networkEvent.Timestamp.After(ts) {
		ts = networkEvent.Timestamp
	}

	fields := mergeFields(apacheEvent, networkEvent)
	if apacheEvent.KeepAliveSeq > 0 {
		fields["keepalives"] = apacheEvent.KeepAliveSeq
	}

	return CorrelatedLog{
		Timestamp:  ts,
		SrcIP:      apacheEvent.SrcIP,
		SrcPort:    apacheEvent.SrcPort,
		DstIP:      coalesceString(apacheEvent.DstIP, networkEvent.DstIP),
		DstPort:    coalesceInt(apacheEvent.DstPort, networkEvent.DstPort),
		Correlated: 1,
		OrphanSide: "",
		Fields:     fields,
	}
}

// extractFields copie l'ensemble des champs bruts d'un événement dans une nouvelle map.
func extractFields(e *NormalizedEvent) map[string]any {
	result := make(map[string]any)
	for k, v := range e.Raw {
		result[k] = v
	}
	return result
}

// mergeFields fusionne les champs bruts de deux événements en préfixant les clés en collision par "a_" et "b_".
func mergeFields(a, b *NormalizedEvent) map[string]any {
	result := make(map[string]any)

	// Start with A fields
	for k, v := range a.Raw {
		result[k] = v
	}

	// Merge B fields with collision handling
	for k, v := range b.Raw {
		if existing, exists := result[k]; exists {
			if reflect.DeepEqual(existing, v) {
				continue
			}

			// Collision with different values: keep both with prefixes
			delete(result, k)
			result["a_"+k] = existing
			result["b_"+k] = v
			continue
		}
		result[k] = v
	}

	return result
}

// coalesceString retourne la première chaîne non vide parmi les deux arguments.
func coalesceString(a, b string) string {
	if a != "" {
		return a
	}
	return b
}

// coalesceInt retourne le premier entier non nul parmi les deux arguments.
func coalesceInt(a, b int) int {
	if a != 0 {
		return a
	}
	return b
}
