package unixsocket

import (
	"context"
	"encoding/json"
	"fmt"
	"math"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

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

const (
	// Maximum datagram size for JSON logs (64KB - Unix datagram limit)
	MaxDatagramSize = 65535
	// Rate limit: max events per second
	MaxEventsPerSecond = 10000
)

// Config holds the Unix socket source configuration.
type Config struct {
	Name              string
	Path              string
	SourceType        string // "A" for Apache/HTTP, "B" for Network, "" for auto-detect
	SocketPermissions os.FileMode
}

// UnixSocketSource reads JSON events from a Unix datagram socket.
type UnixSocketSource struct {
	config   Config
	mu       sync.Mutex
	conn     *net.UnixConn
	done     chan struct{}
	wg       sync.WaitGroup
	stopOnce sync.Once
	logger   *observability.Logger
}

// NewUnixSocketSource creates a new Unix socket source.
func NewUnixSocketSource(config Config) *UnixSocketSource {
	return &UnixSocketSource{
		config: config,
		done:   make(chan struct{}),
		logger: observability.NewLogger("unixsocket:" + config.Name),
	}
}

// SetLogger sets the logger for the source (for debug mode).
func (s *UnixSocketSource) SetLogger(logger *observability.Logger) {
	s.logger = logger.WithFields(map[string]any{"source": s.config.Name})
}

// Name returns the source name.
func (s *UnixSocketSource) Name() string {
	return s.config.Name
}

// Start begins listening on the Unix datagram socket.
func (s *UnixSocketSource) Start(ctx context.Context, eventChan chan<- *domain.NormalizedEvent) error {
	if strings.TrimSpace(s.config.Path) == "" {
		return fmt.Errorf("socket path cannot be empty")
	}

	// Create parent directory if it doesn't exist
	socketDir := filepath.Dir(s.config.Path)
	if err := os.MkdirAll(socketDir, 0755); err != nil {
		return fmt.Errorf("failed to create socket directory %s: %w", socketDir, err)
	}

	// Remove existing socket file if present
	if info, err := os.Stat(s.config.Path); err == nil {
		if info.Mode()&os.ModeSocket != 0 {
			if err := os.Remove(s.config.Path); err != nil {
				return fmt.Errorf("failed to remove existing socket: %w", err)
			}
		} else {
			return fmt.Errorf("path exists but is not a socket: %s", s.config.Path)
		}
	}

	// Create Unix datagram socket
	addr, err := net.ResolveUnixAddr("unixgram", s.config.Path)
	if err != nil {
		return fmt.Errorf("failed to resolve unix socket address: %w", err)
	}

	conn, err := net.ListenUnixgram("unixgram", addr)
	if err != nil {
		return fmt.Errorf("failed to create unix datagram socket: %w", err)
	}
	s.conn = conn

	// Set permissions - fail if we can't
	permissions := s.config.SocketPermissions
	if permissions == 0 {
		permissions = 0666 // default
	}
	if err := os.Chmod(s.config.Path, permissions); err != nil {
		_ = conn.Close()
		_ = os.Remove(s.config.Path)
		return fmt.Errorf("failed to set socket permissions: %w", err)
	}

	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		s.readDatagrams(ctx, eventChan)
	}()

	return nil
}

// readDatagrams lit en continu les datagrammes sur la socket Unix et envoie les événements normalisés sur le canal.
func (s *UnixSocketSource) readDatagrams(ctx context.Context, eventChan chan<- *domain.NormalizedEvent) {
	buf := make([]byte, MaxDatagramSize)

	for {
		select {
		case <-s.done:
			return
		case <-ctx.Done():
			return
		default:
		}

		// Set read deadline to allow periodic context checks
		_ = s.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))

		n, _, err := s.conn.ReadFromUnix(buf)
		if err != nil {
			if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
				// Read timeout, continue to check context
				continue
			}
			// Other errors (e.g., closed socket)
			select {
			case <-s.done:
				return
			case <-ctx.Done():
				return
			default:
				s.logger.Warnf("read error: %v", err)
				continue
			}
		}

		if n == 0 {
			continue
		}

		data := make([]byte, n)
		copy(data, buf[:n])

		event, err := parseJSONEvent(data, s.config.SourceType)
		if err != nil {
			// Log parse errors with the raw data for debugging
			s.logger.Warnf("parse error: %v | raw: %s", err, string(data))
			continue
		}

		// Debug: log raw events with all key details
		s.logger.Debugf("event received: source=%s src_ip=%s src_port=%d timestamp=%v raw_timestamp=%v",
			event.Source, event.SrcIP, event.SrcPort, event.Timestamp, event.Raw["timestamp"])

		select {
		case eventChan <- event:
		case <-ctx.Done():
			return
		}
	}
}

// resolveSource détermine la source d'un événement à partir du type déclaré ou de la présence d'en-têtes HTTP.
func resolveSource(sourceType string, headers map[string]string) domain.EventSource {
	switch strings.ToLower(strings.TrimSpace(sourceType)) {
	case "a", "apache", "http":
		return domain.SourceA
	case "b", "network", "net":
		return domain.SourceB
	default:
		// fallback compat
		if len(headers) > 0 {
			return domain.SourceA
		}
		return domain.SourceB
	}
}

// parseJSONEvent désérialise un datagramme JSON et construit un NormalizedEvent validé avec ses champs obligatoires.
func parseJSONEvent(data []byte, sourceType string) (*domain.NormalizedEvent, error) {
	var raw map[string]any
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("invalid JSON: %w", err)
	}

	event := &domain.NormalizedEvent{
		Raw:     raw,
		Extra:   make(map[string]any),
		Headers: make(map[string]string),
	}

	// Extract headers (header_* fields) first
	for k, v := range raw {
		if strings.HasPrefix(k, "header_") {
			if sv, ok := v.(string); ok {
				event.Headers[k[7:]] = sv
			}
		}
	}

	// Resolve source first (strict timestamp logic depends on source)
	event.Source = resolveSource(sourceType, event.Headers)

	// Extract and validate src_ip
	if v, ok := getString(raw, "src_ip"); ok {
		v = strings.TrimSpace(v)
		if v == "" {
			return nil, fmt.Errorf("src_ip cannot be empty")
		}
		event.SrcIP = v
	} else {
		return nil, fmt.Errorf("missing required field: src_ip")
	}

	// Extract and validate src_port
	if v, ok := getInt(raw, "src_port"); ok {
		if v < 1 || v > 65535 {
			return nil, fmt.Errorf("src_port must be between 1 and 65535, got %d", v)
		}
		event.SrcPort = v
	} else {
		return nil, fmt.Errorf("missing required field: src_port")
	}

	// Extract dst_ip (optional)
	if v, ok := getString(raw, "dst_ip"); ok {
		event.DstIP = strings.TrimSpace(v)
	}

	// Extract dst_port (optional)
	if v, ok := getInt(raw, "dst_port"); ok {
		if v < 0 || v > 65535 {
			return nil, fmt.Errorf("dst_port must be between 0 and 65535, got %d", v)
		}
		event.DstPort = v
	}

	// Extract timestamp based on source contract
	switch event.Source {
	case domain.SourceA:
		ts, ok := getInt64(raw, "timestamp_ns")
		if !ok {
			// Fallback to legacy "timestamp" field name
			ts, ok = getInt64(raw, "timestamp")
		}
		if !ok {
			return nil, fmt.Errorf("missing required numeric field: timestamp/timestamp_ns for source A")
		}
		// Assume nanoseconds
		event.Timestamp = time.Unix(0, ts)
	case domain.SourceB:
		// For network source, try to use event timestamp if available,
		// fallback to reception time. This improves correlation accuracy
		// when network logs include their own timestamp (e.g., from packet capture).
		if ts, ok := getInt64(raw, "timestamp"); ok {
			event.Timestamp = time.Unix(0, ts)
		} else if timeStr, ok := getString(raw, "time"); ok {
			// Try RFC3339 format
			if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
				event.Timestamp = t
			} else if t, err := time.Parse(time.RFC3339Nano, timeStr); err == nil {
				event.Timestamp = t
			} else {
				event.Timestamp = time.Now()
			}
		} else {
			event.Timestamp = time.Now()
		}
	default:
		return nil, fmt.Errorf("unsupported source type: %s", event.Source)
	}

	// Extra fields
	knownFields := map[string]bool{
		"src_ip": true, "src_port": true, "dst_ip": true, "dst_port": true,
		"timestamp": true, "time": true,
	}
	for k, v := range raw {
		if knownFields[k] {
			continue
		}
		if strings.HasPrefix(k, "header_") {
			continue
		}
		event.Extra[k] = v
	}

	return event, nil
}

// getString extrait la valeur d'une clé sous forme de chaîne depuis une map JSON désérialisée.
func getString(m map[string]any, key string) (string, bool) {
	if v, ok := m[key]; ok {
		if s, ok := v.(string); ok {
			return s, true
		}
	}
	return "", false
}

// getInt extrait la valeur d'une clé sous forme d'entier depuis une map JSON en gérant les conversions de types courants.
func getInt(m map[string]any, key string) (int, bool) {
	if v, ok := m[key]; ok {
		switch val := v.(type) {
		case float64:
			if math.Trunc(val) != val {
				return 0, false
			}
			return int(val), true
		case int:
			return val, true
		case int64:
			return int(val), true
		case string:
			if i, err := strconv.Atoi(val); err == nil {
				return i, true
			}
		}
	}
	return 0, false
}

// getInt64 extrait la valeur d'une clé sous forme d'entier 64 bits depuis une map JSON en gérant les conversions de types courants.
func getInt64(m map[string]any, key string) (int64, bool) {
	if v, ok := m[key]; ok {
		switch val := v.(type) {
		case float64:
			if math.Trunc(val) != val {
				return 0, false
			}
			return int64(val), true
		case int:
			return int64(val), true
		case int64:
			return val, true
		case string:
			if i, err := strconv.ParseInt(val, 10, 64); err == nil {
				return i, true
			}
		}
	}
	return 0, false
}

// Stop gracefully stops the source.
func (s *UnixSocketSource) Stop() error {
	var stopErr error

	s.stopOnce.Do(func() {
		s.mu.Lock()
		defer s.mu.Unlock()

		close(s.done)

		if s.conn != nil {
			_ = s.conn.Close()
		}

		s.wg.Wait()

		// Clean up socket file
		if err := os.Remove(s.config.Path); err != nil && !os.IsNotExist(err) {
			stopErr = fmt.Errorf("failed to remove socket file: %w", err)
			return
		}
	})

	return stopErr
}
