// Package main est le point d'entrée du démon ja4ebpf.
// Il initialise la configuration, charge les programmes eBPF, démarre
// les goroutines de traitement et gère les signaux système.
package main

import (
	"context"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/antitbone/ja4/ja4ebpf/internal/correlation"
	"github.com/antitbone/ja4/ja4ebpf/internal/loader"
	"github.com/antitbone/ja4/ja4ebpf/internal/parser"
	"github.com/antitbone/ja4/ja4ebpf/internal/procutil"
	"github.com/antitbone/ja4/ja4ebpf/internal/writer"
	"github.com/cilium/ebpf/ringbuf"
	"gopkg.in/yaml.v3"
)

// fdCache résout les associations fd → IP:port via /proc quand accept4 n'est pas disponible.
// Durée de vie d'une entrée : 5 secondes (suffisant pour une requête HTTP).
var fdCache = procutil.NewFDCache(5 * time.Second)

// Config décrit la configuration complète du démon ja4ebpf.
// Chargée depuis un fichier YAML et enrichie par les variables d'environnement
// avec le préfixe JA4EBPF_.
type Config struct {
	Interface  string `yaml:"interface"`    // interface réseau à surveiller (ex: "eth0")
	SSLLibPath string `yaml:"ssl_lib_path"` // chemin vers libssl (ex: "/usr/lib64/libssl.so.3")

	ClickHouse struct {
		DSN       string `yaml:"dsn"`        // DSN ClickHouse natif
		BatchSize int    `yaml:"batch_size"` // nombre de sessions par batch
		FlushSecs int    `yaml:"flush_secs"` // intervalle de flush en secondes
	} `yaml:"clickhouse"`

	Correlation struct {
		TimeoutMS   int `yaml:"timeout_ms"`   // délai d'expiration session (ms)
		SlowlorisMS int `yaml:"slowloris_ms"` // seuil Slowloris (ms)
	} `yaml:"correlation"`

	Log struct {
		Level  string `yaml:"level"`  // niveau de log (debug, info, warn, error)
		Format string `yaml:"format"` // format de log ("json" ou "text")
	} `yaml:"log"`
}

// loadConfig charge la configuration depuis le fichier YAML spécifié,
// puis applique les surcharges depuis les variables d'environnement.
func loadConfig(path string) (*Config, error) {
	cfg := &Config{}

	// Valeurs par défaut
	cfg.Interface  = "eth0"
	cfg.SSLLibPath = "/usr/lib64/libssl.so.3"
	cfg.ClickHouse.DSN       = "clickhouse://default:@localhost:9000/ja4_logs"
	cfg.ClickHouse.BatchSize = 500
	cfg.ClickHouse.FlushSecs = 1
	cfg.Correlation.TimeoutMS   = 500
	cfg.Correlation.SlowlorisMS = 10000
	cfg.Log.Level  = "info"
	cfg.Log.Format = "json"

	// Charger depuis le fichier YAML si spécifié
	if path != "" {
		data, err := os.ReadFile(path)
		if err != nil {
			return nil, fmt.Errorf("lecture fichier config %q: %w", path, err)
		}
		if err := yaml.Unmarshal(data, cfg); err != nil {
			return nil, fmt.Errorf("parsing YAML config: %w", err)
		}
	}

	// Surcharges via variables d'environnement
	if v := os.Getenv("JA4EBPF_INTERFACE"); v != "" {
		cfg.Interface = v
	}
	if v := os.Getenv("JA4EBPF_SSL_LIB_PATH"); v != "" {
		cfg.SSLLibPath = v
	}
	if v := os.Getenv("JA4EBPF_CLICKHOUSE_DSN"); v != "" {
		cfg.ClickHouse.DSN = v
	}

	return cfg, nil
}

// main est le point d'entrée du programme.
func main() {
	// Déterminer le chemin du fichier de configuration
	configPath := os.Getenv("JA4EBPF_CONFIG")
	if configPath == "" {
		configPath = "/etc/ja4ebpf/config.yml"
	}

	cfg, err := loadConfig(configPath)
	if err != nil {
		log.Fatalf("erreur chargement configuration: %v", err)
	}

	log.Printf("[ja4ebpf] démarrage — interface=%s ssl=%s", cfg.Interface, cfg.SSLLibPath)

	// Contexte principal avec annulation sur signal système
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Intercepter SIGTERM et SIGINT pour l'arrêt gracieux
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)

	// --- 1. Chargement des programmes eBPF ---
	ldr, err := loader.New()
	if err != nil {
		log.Fatalf("erreur chargement eBPF: %v", err)
	}
	defer ldr.Close()

	// --- 2. Attachement TC ingress ---
	if err := ldr.AttachTC(cfg.Interface); err != nil {
		log.Fatalf("erreur attachement TC sur %s: %v", cfg.Interface, err)
	}

	// --- 3. Attachement uprobes SSL ---
	if err := ldr.AttachUprobes(cfg.SSLLibPath); err != nil {
		log.Printf("[ja4ebpf] avertissement uprobes SSL: %v (désactivation uprobes)", err)
		// Continuer sans uprobes SSL (capture L3/L4 toujours active)
	}

	// --- 4. Attachement tracepoints accept4 (sys_enter/exit_accept4) ---
	if err := ldr.AttachAcceptProbe(); err != nil {
		log.Printf("[ja4ebpf] avertissement tracepoint accept4: %v", err)
	}

	// --- 5. Gestionnaire de sessions ---
	sessionTimeout := time.Duration(cfg.Correlation.TimeoutMS) * time.Millisecond
	mgr := correlation.NewManager(sessionTimeout)
	mgr.StartGC(ctx)
	defer mgr.Close()

	// --- 6. Writer ClickHouse ---
	flushInterval := time.Duration(cfg.ClickHouse.FlushSecs) * time.Second
	w, err := writer.NewClickHouseWriter(cfg.ClickHouse.DSN, cfg.ClickHouse.BatchSize, flushInterval)
	if err != nil {
		log.Fatalf("erreur initialisation writer ClickHouse: %v", err)
	}
	w.Start(ctx)

	// --- 7. Goroutine : écriture des sessions prêtes ---
	go func() {
		for s := range mgr.ReadyCh {
			w.Write(s)
		}
	}()

	// --- 8. Goroutines de consommation des ring buffers ---
	go consumeSynEvents(ctx, ldr.SynReader, mgr)
	go consumeTLSEvents(ctx, ldr.TLSReader, mgr)
	go consumeSSLEvents(ctx, ldr.SSLReader, mgr)
	go consumeAcceptEvents(ctx, ldr.AcceptReader, mgr)
	go consumeHTTPPlainEvents(ctx, ldr.HTTPPlainReader, mgr)

	log.Printf("[ja4ebpf] démon actif — en attente des événements")

	// Attendre un signal d'arrêt
	select {
	case sig := <-sigCh:
		log.Printf("[ja4ebpf] signal reçu: %v — arrêt gracieux", sig)
	case <-ctx.Done():
	}

	cancel()
	log.Printf("[ja4ebpf] arrêt terminé")
}

// parseTCPOptions extrait le MSS et le Window Scale depuis les options TCP brutes.
// Les options TCP suivent le format TLV (Type-Length-Value), sauf les options 0 et 1.
// Retourne (mss=0, windowScale=0xFF) si les options sont absentes ou mal formées.
func parseTCPOptions(opts []byte) (mss uint16, windowScale uint8) {
	windowScale = 0xFF // 0xFF = absent (convention C)
	i := 0
	for i < len(opts) {
		kind := opts[i]
		i++
		switch kind {
		case 0: // End of Options
			return
		case 1: // NOP — padding, pas de longueur
			continue
		default:
			if i >= len(opts) {
				return
			}
			length := int(opts[i])
			i++
			if length < 2 || i+length-2 > len(opts) {
				return // option malformée
			}
			val := opts[i : i+length-2]
			switch kind {
			case 2: // MSS
				if len(val) >= 2 {
					mss = binary.BigEndian.Uint16(val[0:2])
				}
			case 3: // Window Scale
				if len(val) >= 1 {
					windowScale = val[0]
				}
			}
			i += length - 2
		}
	}
	return
}

// consumeSynEvents lit les événements TCP SYN depuis le ring buffer
// et met à jour l'état L3/L4 des sessions.
func consumeSynEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		record, err := rd.Read()
		if err != nil {
			if err == ringbuf.ErrClosed {
				return
			}
			continue
		}

		// struct tcp_syn_event (packed):
		//   src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+ttl(1)+df_bit(1)+ip_id(2)+
		//   window_size(2)+window_scale(1)+mss(2)+tcp_options_raw[40]+tcp_options_len(1)+timestamp_ns(8)
		//   offsets: 0   4    8    10   12    13    14    16    18    19   21              61           62
		if len(record.RawSample) < 62 {
			continue
		}
		data := record.RawSample

		// src_ip et src_port stockés en host byte order (bpf_ntohl/bpf_ntohs dans BPF C).
		srcIPRaw := binary.LittleEndian.Uint32(data[0:4])
		srcPort  := binary.LittleEndian.Uint16(data[8:10])

		var key correlation.SessionKey
		key.SrcIP[0] = byte(srcIPRaw >> 24)
		key.SrcIP[1] = byte(srcIPRaw >> 16)
		key.SrcIP[2] = byte(srcIPRaw >> 8)
		key.SrcIP[3] = byte(srcIPRaw)
		key.SrcPort  = srcPort

		// Champs IP/TCP aux offsets corrects (dst_ip occupe les octets 4-7)
		ttl        := data[12]
		dfBit      := data[13] != 0
		ipID       := binary.LittleEndian.Uint16(data[14:16])
		windowSize := binary.LittleEndian.Uint16(data[16:18])

		optLen := int(data[61])
		if optLen > 40 {
			optLen = 40
		}
		tcpOpts := make([]byte, optLen)
		copy(tcpOpts, data[21:21+optLen])

		// Analyser les options TCP brutes pour extraire MSS et Window Scale
		mss, windowScale := parseTCPOptions(tcpOpts)

		mgr.Update(key, func(s *correlation.SessionState) {
			s.L3L4 = &correlation.L3L4{
				TTL:           ttl,
				DFBit:         dfBit,
				IPID:          ipID,
				WindowSize:    windowSize,
				WindowScale:   windowScale,
				MSS:           mss,
				TCPOptionsRaw: tcpOpts,
				SYNTimestamp:  time.Now(),
			}
			// Si TLS est déjà présent (arrivé avant SYN), les deux couches sont disponibles.
			if s.TLS != nil {
				_ = s.TLS // corrélation implicite par présence des deux champs
			}
		})
	}
}

// consumeTLSEvents lit les événements TLS ClientHello depuis le ring buffer
// et calcule l'empreinte JA4 pour chaque session.
func consumeTLSEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		record, err := rd.Read()
		if err != nil {
			if err == ringbuf.ErrClosed {
				return
			}
			continue
		}

		// struct tls_hello_event (packed):
		//   src_ip(4) + src_port(2) + payload[2048] + payload_len(2) + timestamp_ns(8)
		//   offsets: 0            4              6                2054           2056
		if len(record.RawSample) < 2056 {
			continue
		}
		data := record.RawSample

		srcIPRaw   := binary.LittleEndian.Uint32(data[0:4])
		srcPort    := binary.LittleEndian.Uint16(data[4:6])
		payloadLen := binary.LittleEndian.Uint16(data[2054:2056])

		if int(payloadLen) > 2048 {
			payloadLen = 2048
		}
		payload := make([]byte, payloadLen)
		copy(payload, data[6:6+payloadLen])

		var key correlation.SessionKey
		key.SrcIP[0] = byte(srcIPRaw >> 24)
		key.SrcIP[1] = byte(srcIPRaw >> 16)
		key.SrcIP[2] = byte(srcIPRaw >> 8)
		key.SrcIP[3] = byte(srcIPRaw)
		key.SrcPort  = srcPort

		// Parser le ClientHello et calculer JA4
		ch, err := parser.ParseClientHello(payload)
		if err != nil {
			log.Printf("[warn] TLS parse error: %v", err)
			continue
		}

		ja4 := parser.ComputeJA4(ch)

		var alpn []string
		var ciphers, extensions []uint16
		for _, e := range ch.Extensions {
			extensions = append(extensions, e.Type)
		}
		ciphers = ch.CipherSuites
		alpn    = ch.ALPN

		mgr.Update(key, func(s *correlation.SessionState) {
			s.TLS = &correlation.TLSInfo{
				ClientHelloRaw: payload,
				JA4Hash:        ja4,
				SNI:            ch.SNI,
				ALPN:           alpn,
				CipherSuites:   ciphers,
				Extensions:     extensions,
				Timestamp:      time.Now(),
			}
			// Corréler si L3/L4 est déjà présent
			if s.L3L4 != nil {
				_ = s.L3L4 // corrélation implicite par présence des deux champs
			}
		})
	}
}

// consumeSSLEvents lit les données SSL déchiffrées depuis le ring buffer.
// Parse les requêtes HTTP/1.x et détecte le préambule HTTP/2.
// Quand src_ip=0 (accept4 non disponible), tente un lookup /proc pour retrouver l'IP du client.
func consumeSSLEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		record, err := rd.Read()
		if err != nil {
			if err == ringbuf.ErrClosed {
				return
			}
			continue
		}

		data := record.RawSample
		// Taille minimale : pid_tgid(8) + fd(4) + src_ip(4) + src_port(2) = 18
		if len(data) < 18 {
			continue
		}

		pidTgid := binary.LittleEndian.Uint64(data[0:8])
		fd      := binary.LittleEndian.Uint32(data[8:12])
		srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
		srcPort  := binary.LittleEndian.Uint16(data[16:18])

		// data[4096] commence à offset 18, data_len à offset 4114
		if len(data) < 4118 {
			continue
		}
		dataLen := binary.LittleEndian.Uint32(data[4114:4118])
		if dataLen > 4096 {
			dataLen = 4096
		}
		if dataLen == 0 {
			continue
		}
		sslData := data[18 : 18+dataLen]

		// --- Fallback /proc quand accept4 n'a pas fourni l'IP ---
		if srcIPRaw == 0 && fd != 0 {
			tgid := uint32(pidTgid >> 32)
			if tgid == 0 {
				tgid = uint32(pidTgid) // fallback: utiliser le TID si TGID=0
			}
			if ip, port, lookupErr := fdCache.Lookup(tgid, fd); lookupErr == nil {
				ipv4 := ip.To4()
				if ipv4 != nil {
					srcIPRaw = uint32(ipv4[0])<<24 | uint32(ipv4[1])<<16 | uint32(ipv4[2])<<8 | uint32(ipv4[3])
					srcPort  = port
				}
			}
		}

		// Ignorer les événements sans IP identifiable (ex: connexions locales non HTTP)
		if srcIPRaw == 0 && srcPort == 0 {
			continue
		}

		var key correlation.SessionKey
		key.SrcIP[0] = byte(srcIPRaw >> 24)
		key.SrcIP[1] = byte(srcIPRaw >> 16)
		key.SrcIP[2] = byte(srcIPRaw >> 8)
		key.SrcIP[3] = byte(srcIPRaw)
		key.SrcPort  = srcPort

		// === Routeur Magic Bytes ===

		if parser.DetectH2Preface(sslData) {
			// HTTP/2 : extraire les paramètres SETTINGS depuis la préface
			afterPreface := sslData
			if len(afterPreface) > parser.H2MagicPrefaceLen() {
				afterPreface = sslData[parser.H2MagicPrefaceLen():]
			}
			h2settings, err := parser.ParseH2ClientPreface(afterPreface)
			if err != nil {
				continue
			}
			mgr.Update(key, func(s *correlation.SessionState) {
				req := correlation.HTTPRequest{
					Timestamp: time.Now(),
				}
				if h2settings != nil {
					req.HTTP2Settings = &correlation.HTTP2Settings{
						HeaderTableSize:       h2settings.HeaderTableSize,
						EnablePush:            h2settings.EnablePush,
						MaxConcurrentStreams:  h2settings.MaxConcurrentStreams,
						InitialWindowSize:     h2settings.InitialWindowSize,
						MaxFrameSize:          h2settings.MaxFrameSize,
						MaxHeaderListSize:     h2settings.MaxHeaderListSize,
						UnknownSettings:       h2settings.UnknownSettings,
						WindowUpdateIncrement: h2settings.WindowUpdateIncrement,
						PseudoHeaderOrder:     h2settings.PseudoHeaderOrder,
					}
				}
				if len(s.Requests) == 0 {
					s.Requests = append(s.Requests, req)
				}
				_ = s.TLS // corrélation implicite
			})
			continue
		}

		if parser.IsHTTP1Request(sslData) {
			// HTTP/1.x : parser la requête
			req := parser.ParseHTTP1Request(sslData)
			if req == nil {
				continue
			}
			mgr.Update(key, func(s *correlation.SessionState) {
				s.Requests = append(s.Requests, correlation.HTTPRequest{
					Timestamp:        time.Now(),
					Method:           req.Method,
					Path:             req.Path,
					QueryString:      req.Query,
					HeaderOrder:      req.Headers,
					HeaderOrderSig:   req.HeaderSig,
				})
				_ = s.TLS // corrélation implicite
			})
			continue
		}

		if parser.IsHTTP1Response(sslData) {
			// Réponse HTTP/1.x : extraire le code de statut
			resp := parser.ParseHTTP1Response(sslData)
			if resp == nil {
				continue
			}
			mgr.Update(key, func(s *correlation.SessionState) {
				// Mettre à jour le code de statut de la dernière requête
				if len(s.Requests) > 0 {
					last := &s.Requests[len(s.Requests)-1]
					if last.StatusCode == 0 {
						last.StatusCode = resp.StatusCode
					}
				}
			})
		}
	}
}

// consumeAcceptEvents lit les événements accept4 depuis le ring buffer.
// Met à jour les sessions avec les informations de connexion client.
func consumeAcceptEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		record, err := rd.Read()
		if err != nil {
			if err == ringbuf.ErrClosed {
				return
			}
			continue
		}

		data := record.RawSample
		// Taille attendue : pid_tgid(8) + fd(4) + src_ip(4) + src_port(2) + timestamp(8) = 26
		if len(data) < 22 {
			continue
		}

		srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
		srcPort  := binary.LittleEndian.Uint16(data[16:18])

		var key correlation.SessionKey
		key.SrcIP[0] = byte(srcIPRaw >> 24)
		key.SrcIP[1] = byte(srcIPRaw >> 16)
		key.SrcIP[2] = byte(srcIPRaw >> 8)
		key.SrcIP[3] = byte(srcIPRaw)
		key.SrcPort  = srcPort

		// Ignorer les événements accept4 sans IP valide (bpf_probe_read_user a échoué)
		if srcIPRaw == 0 && srcPort == 0 {
			continue
		}

		// S'assurer que la session existe
		mgr.GetOrCreate(key)
	}
}

// consumeHTTPPlainEvents lit les payloads HTTP en clair depuis le ring buffer XDP.
// Parse la requête HTTP/1.x ou détecte la préface HTTP/2 pour les connexions
// non-chiffrées sur les ports 80/8080.
func consumeHTTPPlainEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		record, err := rd.Read()
		if err != nil {
			if err == ringbuf.ErrClosed {
				return
			}
			continue
		}

		data := record.RawSample
		// struct http_plain_event: src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+payload(4096)+payload_len(2)+timestamp_ns(8)
		if len(data) < 14 {
			continue
		}

		// src_ip et src_port en host byte order (bpf_ntohl appliqué dans tc_capture.c)
		srcIPRaw := binary.LittleEndian.Uint32(data[0:4])
		srcPort  := binary.LittleEndian.Uint16(data[8:10])

		if srcIPRaw == 0 && srcPort == 0 {
			continue
		}

		var key correlation.SessionKey
		key.SrcIP[0] = byte(srcIPRaw >> 24)
		key.SrcIP[1] = byte(srcIPRaw >> 16)
		key.SrcIP[2] = byte(srcIPRaw >> 8)
		key.SrcIP[3] = byte(srcIPRaw)
		key.SrcPort  = srcPort

		// Extraire le payload HTTP
		if len(data) < 4110 {
			continue
		}
		payloadLen := int(binary.LittleEndian.Uint16(data[4108:4110]))
		if payloadLen > 4096 {
			payloadLen = 4096
		}
		if payloadLen == 0 {
			continue
		}
		if 12+payloadLen > len(data) {
			payloadLen = len(data) - 12
		}
		httpData := data[12 : 12+payloadLen]

		// Routeur Magic Bytes : HTTP/1.x uniquement sur port 80
		if parser.IsHTTP1Request(httpData) {
			req := parser.ParseHTTP1Request(httpData)
			if req == nil {
				continue
			}
			mgr.Update(key, func(s *correlation.SessionState) {
				s.Requests = append(s.Requests, correlation.HTTPRequest{
					Timestamp:      time.Now(),
					Method:         req.Method,
					Path:           req.Path,
					QueryString:    req.Query,
					HeaderOrder:    req.Headers,
					HeaderOrderSig: req.HeaderSig,
				})
				// Corréler si L3/L4 est déjà présent (TCP SYN capturé)
				_ = s.L3L4 // corrélation implicite
			})
		}
	}
}
