//go:build windows

package main

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"syscall"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/debug"
	"golang.org/x/sys/windows/svc/eventlog"
	"golang.org/x/sys/windows/svc/mgr"
)

const (
	ServiceName = "SENDNRW-SIEM-AGENT"

	BatchSize      = 8
	PollWaitMS     = 2000
	FlushInterval  = 5 * time.Second
	HTTPTimeout    = 10 * time.Second
	ServiceLogInfo = 1
)

const (
	ERROR_EVT_INVALID_OPERATION syscall.Errno = 15010
)

const AgentConfigPath = `C:\ProgramData\WinEventForwarder\agent.json`

type ChannelConfig struct {
	Name string
	IDs  map[uint32]bool
}

type AgentConfig struct {
	BackendURL    string        `json:"backend_url"`
	EnrollmentKey string        `json:"enrollment_key"`
	ChannelRules  []ChannelRule `json:"channel_rules"`
	StateFile     string        `json:"state_file"`
}

type ChannelRule struct {
	Name string   `json:"name"`
	IDs  []uint32 `json:"ids"`
}

type AgentState struct {
	Hostname   string    `json:"hostname"`
	APIKey     string    `json:"api_key"`
	Enrolled   bool      `json:"enrolled"`
	EnrolledAt time.Time `json:"enrolled_at,omitempty"`
}

type LogPayload struct {
	Hostname string    `json:"host"`
	Channel  string    `json:"channel"`
	EventID  uint32    `json:"id"`
	Source   string    `json:"source"`
	Time     time.Time `json:"ts"`
	Message  string    `json:"msg"`
}

type eventXML struct {
	System struct {
		Provider struct {
			Name string `xml:"Name,attr"`
		} `xml:"Provider"`
		EventID     uint32 `xml:"EventID"`
		Computer    string `xml:"Computer"`
		TimeCreated struct {
			SystemTime string `xml:"SystemTime,attr"`
		} `xml:"TimeCreated"`
	} `xml:"System"`
}

var (
	modWevtapi       = windows.NewLazySystemDLL("wevtapi.dll")
	procEvtSubscribe = modWevtapi.NewProc("EvtSubscribe")
	procEvtNext      = modWevtapi.NewProc("EvtNext")
	procEvtRender    = modWevtapi.NewProc("EvtRender")
	procEvtClose     = modWevtapi.NewProc("EvtClose")
)

const (
	evtSubscribeToFutureEvents = 1
	evtRenderEventXML          = 1
)

type myservice struct {
	cfg   *AgentConfig
	state *AgentState
}

func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
	status <- svc.Status{State: svc.StartPending}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	hostname, _ := os.Hostname()
	client := &http.Client{Timeout: HTTPTimeout}
	out := make(chan LogPayload, 256)

	sendInitialHello(client, m.cfg, m.state, hostname)

	var wg sync.WaitGroup

	// Sender
	wg.Add(1)
	go func() {
		defer wg.Done()
		runSender(
			ctx,
			client,
			m.cfg.BackendURL,
			m.cfg.EnrollmentKey,
			m.cfg.StateFile,
			m.state,
			out,
		)
	}()

	// Channel-Worker
	runtimeChannels := buildChannelConfigs(m.cfg.ChannelRules)

	for _, chCfg := range runtimeChannels {
		chCfg := chCfg

		chunks := splitIDMap(chCfg.IDs, 20)
		for i, idsChunk := range chunks {
			workerCfg := ChannelConfig{
				Name: chCfg.Name,
				IDs:  idsChunk,
			}

			wg.Add(1)
			go func(chunkNo int, cfg ChannelConfig) {
				defer wg.Done()
				log.Printf("[%s] Starte Watcher-Chunk %d/%d mit %d IDs",
					cfg.Name, chunkNo+1, len(chunks), len(cfg.IDs))
				runChannelWatcher(ctx, hostname, cfg, out)
				log.Println("Prozess fertig oder abgebrochen", cfg)
			}(i, workerCfg)
		}
	}

	status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}

loop:
	for c := range r { // "for range" is what Staticcheck prefers
		switch c.Cmd {
		case svc.Interrogate:
			status <- c.CurrentStatus
		case svc.Stop, svc.Shutdown:
			break loop
		}
	}

	cancel()
	wg.Wait()
	status <- svc.Status{State: svc.Stopped}
	return false, 0
}

func main() {

	if err := setupFileLogging(); err != nil {
		fmt.Printf("Logging-Setup fehlgeschlagen: %v\n", err)
	}

	cfg, state, hostname, err := initAgent(AgentConfigPath)
	if err != nil {
		log.Fatalf("Agent-Initialisierung fehlgeschlagen: %v", err)
	}

	fmt.Printf("Agent gestartet auf %s\n", hostname)
	fmt.Printf("Backend: %s\n", cfg.BackendURL)
	fmt.Printf("Enrolled: %v\n", state.Enrolled)

	isService, err := svc.IsWindowsService()
	if err != nil {
		log.Fatalf("svc.IsWindowsService: %v", err)
	}

	if len(os.Args) > 1 {
		switch strings.ToLower(os.Args[1]) {
		case "install":
			if err := installService(); err != nil {
				log.Fatalf("Install fehlgeschlagen: %v", err)
			}
			fmt.Println("Dienst installiert.")
			return
		case "remove", "uninstall":
			if err := removeService(); err != nil {
				log.Fatalf("Entfernen fehlgeschlagen: %v", err)
			}
			fmt.Println("Dienst entfernt.")
			return
		case "debug":
			runDebug(cfg, state)
			return
		}
	}

	if !isService {
		fmt.Println("Interaktive Sitzung erkannt. Starte im Debug-Modus.")
		runDebug(cfg, state)
		return
	}

	if err := svc.Run(ServiceName, &myservice{
		cfg:   cfg,
		state: state,
	}); err != nil {
		log.Fatalf("svc.Run: %v", err)
	}
}

func runDebug(cfg *AgentConfig, state *AgentState) {
	elog := debug.New(ServiceName)
	defer elog.Close()

	hostname, _ := os.Hostname()
	client := &http.Client{Timeout: HTTPTimeout}
	out := make(chan LogPayload, 256)

	sendInitialHello(client, cfg, state, hostname)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		runSender(
			ctx,
			client,
			cfg.BackendURL,
			cfg.EnrollmentKey,
			cfg.StateFile,
			state,
			out,
		)
	}()

	runtimeChannels := buildChannelConfigs(cfg.ChannelRules)

	for _, chCfg := range runtimeChannels {
		chCfg := chCfg

		chunks := splitIDMap(chCfg.IDs, 20)
		for i, idsChunk := range chunks {
			workerCfg := ChannelConfig{
				Name: chCfg.Name,
				IDs:  idsChunk,
			}

			wg.Add(1)
			go func(chunkNo int, cfg ChannelConfig) {
				defer wg.Done()
				log.Printf("[%s] Starte Watcher-Chunk %d/%d mit %d IDs",
					cfg.Name, chunkNo+1, len(chunks), len(cfg.IDs))
				runChannelWatcher(ctx, hostname, cfg, out)
			}(i, workerCfg)
		}
	}

	elog.Info(ServiceLogInfo, "Debug-Modus läuft. Mit Strg+C beenden.")

	// Create a channel to listen for Windows signals (like Ctrl+C)
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

	select {
	case <-sigChan:
		elog.Info(ServiceLogInfo, "Beenden-Signal empfangen...")
	case <-ctx.Done():
	}

	cancel()  // Trigger shutdown for workers
	wg.Wait() // Wait for workers to clean up
}

func runChannelWatcher(ctx context.Context, hostname string, cfg ChannelConfig, out chan<- LogPayload) {
	query := buildXPathQuery2(cfg.IDs)

	signalEvent, err := windows.CreateEvent(nil, 1, 0, nil)
	if err != nil {
		log.Printf("[%s] CreateEvent-Fehler: %v", cfg.Name, err)
		return
	}
	defer windows.CloseHandle(signalEvent)

	sub, err := evtSubscribe(cfg.Name, query, signalEvent, evtSubscribeToFutureEvents)
	if err != nil {
		log.Printf("[%s] Subscribe-Fehler: %v | Query=%s", cfg.Name, err, query)
		return
	}
	defer evtClose(sub)

	log.Printf("[%s] Überwachung gestartet mit Filter %s", cfg.Name, query)

	drain := func() {
		for {
			events, err := evtNext(sub, BatchSize, 0)
			if err != nil {
				code := winErrCode(err)

				// Diese Fehler sind bei deinem Polling nicht fatal.
				if code == windows.ERROR_TIMEOUT ||
					code == windows.ERROR_NO_MORE_ITEMS ||
					strings.Contains(strings.ToLower(err.Error()), "operation identifier is not valid") {
					return
				}

				log.Printf("[%s] EvtNext-Fehler: %v | Code=%d", cfg.Name, err, uint32(code))
				return
			}

			if len(events) == 0 {
				return
			}

			log.Printf("[%s] %d neue Events gefunden", cfg.Name, len(events))

			for _, h := range events {
				payload, err := buildPayloadFromEventHandle(hostname, cfg.Name, h)
				_ = evtClose(h)

				if err != nil {
					log.Printf("[%s] Event-Verarbeitung fehlgeschlagen: %v", cfg.Name, err)
					continue
				}

				if !cfg.IDs[payload.EventID] {
					log.Printf("[%s] EventID %d ignoriert, nicht in Filter", cfg.Name, payload.EventID)
					continue
				}

				log.Printf("[%s] Event erkannt: ID=%d Source=%s Time=%s",
					cfg.Name,
					payload.EventID,
					payload.Source,
					payload.Time.Format(time.RFC3339),
				)

				select {
				case out <- payload:
				case <-ctx.Done():
					return
				}
			}
		}
	}

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

		waitStatus, err := windows.WaitForSingleObject(signalEvent, PollWaitMS)
		if err != nil {
			log.Printf("[%s] WaitForSingleObject-Fehler: %v", cfg.Name, err)
			time.Sleep(2 * time.Second)
			continue
		}

		if waitStatus == uint32(windows.WAIT_OBJECT_0) {
			_ = windows.ResetEvent(signalEvent)
			drain()
			continue
		}

		if waitStatus == uint32(windows.WAIT_TIMEOUT) {
			// Wichtig: bei dir nötig, weil das Signal offenbar nicht zuverlässig kommt.
			drain()
			continue
		}
		_ = windows.ResetEvent(signalEvent)
		log.Printf("[%s] Unerwarteter Wait-Status: %d", cfg.Name, waitStatus)
		time.Sleep(2 * time.Second)
	}
}

func winErrCode(err error) syscall.Errno {
	var errno syscall.Errno
	if errors.As(err, &errno) {
		return errno
	}
	return 0
}

func runSender(
	ctx context.Context,
	client *http.Client,
	backendURL string,
	enrollmentKey string,
	stateFile string,
	state *AgentState,
	in <-chan LogPayload,
) {
	ticker := time.NewTicker(FlushInterval)
	defer ticker.Stop()

	var batch []LogPayload

	flush := func() {
		if len(batch) == 0 {
			return
		}
		log.Printf("Flush: sende %d Events an %s", len(batch), backendURL)
		ok, err := sendBatch(client, backendURL, state, enrollmentKey, batch)
		if err != nil {
			log.Printf("sendBatch Fehler: %v", err)
			return
		}

		if ok {
			if !state.Enrolled {
				state.Enrolled = true
				state.EnrolledAt = time.Now().UTC()

				if err := saveAgentState(stateFile, state); err != nil {
					log.Printf("state speichern fehlgeschlagen: %v", err)
				} else {
					log.Printf("Agent erfolgreich enrolled")
				}
			}

			log.Printf("%d Events erfolgreich gesendet.", len(batch))
			batch = nil
		}
	}

	for {
		select {
		case <-ctx.Done():
			flush()
			return

		case item, ok := <-in:
			if !ok {
				flush()
				return
			}
			batch = append(batch, item)
			if len(batch) >= 25 {
				flush()
			}

		case <-ticker.C:
			flush()
		}
	}
}

func buildPayloadFromEventHandle(hostname, channel string, h windows.Handle) (LogPayload, error) {
	xmlStr, err := evtRenderXML(h)
	if err != nil {
		return LogPayload{}, fmt.Errorf("render xml: %w", err)
	}

	id, source, ts, err := extractEventMeta(xmlStr)
	if err != nil {
		return LogPayload{}, fmt.Errorf("parse xml: %w", err)
	}

	return LogPayload{
		Hostname: hostname,
		Channel:  channel,
		EventID:  id,
		Source:   source,
		Time:     ts,
		Message:  xmlStr,
	}, nil
}

func extractEventMeta(xmlStr string) (uint32, string, time.Time, error) {
	var evt eventXML
	if err := xml.Unmarshal([]byte(xmlStr), &evt); err != nil {
		return 0, "", time.Time{}, err
	}

	source := evt.System.Provider.Name
	if source == "" {
		source = "unknown"
	}

	ts := time.Now()
	if evt.System.TimeCreated.SystemTime != "" {
		if parsed, err := parseWindowsSystemTime(evt.System.TimeCreated.SystemTime); err == nil {
			ts = parsed
		}
	}

	return evt.System.EventID, source, ts, nil
}

func parseWindowsSystemTime(v string) (time.Time, error) {
	layouts := []string{
		time.RFC3339Nano,
		"2006-01-02T15:04:05.9999999Z07:00",
		"2006-01-02T15:04:05.999999Z07:00",
		"2006-01-02T15:04:05Z07:00",
	}
	for _, layout := range layouts {
		if t, err := time.Parse(layout, v); err == nil {
			return t, nil
		}
	}
	return time.Time{}, fmt.Errorf("unbekanntes Zeitformat: %s", v)
}

func buildXPathQuery(ids map[uint32]bool) string {
	if len(ids) == 0 {
		return "*"
	}

	list := make([]int, 0, len(ids))
	for id := range ids {
		list = append(list, int(id))
	}
	sort.Ints(list)

	parts := make([]string, 0, len(list))
	for _, id := range list {
		// Correct syntax: EventID=1074
		parts = append(parts, fmt.Sprintf("EventID=%d", id))
	}

	// Ensure the structure is exactly: *[System[(EventID=1074 or EventID=6005)]]
	return fmt.Sprintf("*[System[(%s)]]", strings.Join(parts, " or "))
}

func buildXPathQuery2(ids map[uint32]bool) string {
	if len(ids) == 0 {
		return "*"
	}

	list := make([]int, 0, len(ids))
	for id := range ids {
		list = append(list, int(id))
	}
	sort.Ints(list)

	parts := make([]string, 0, len(list))
	for _, id := range list {
		// Manche Windows-Versionen bevorzugen EventID ohne System/ davor
		// innerhalb des System-Knotens.
		parts = append(parts, fmt.Sprintf("EventID=%d", id))
	}

	// WICHTIG: Keine unnötigen Leerzeichen innerhalb der XPath-Klammern
	return fmt.Sprintf("*[System[(%s)]]", strings.Join(parts, " or "))
}

func sendBatch(client *http.Client, backendURL string, state *AgentState, enrollmentKey string, batch []LogPayload) (bool, error) {
	data, err := json.Marshal(batch)
	if err != nil {
		return false, err
	}

	req, err := http.NewRequest(http.MethodPost, backendURL, bytes.NewBuffer(data))
	if err != nil {
		return false, err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-API-Key", state.APIKey)

	if !state.Enrolled {
		req.Header.Set("X-Enrollment-Key", enrollmentKey)
	}

	resp, err := client.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()
	log.Printf("Backend Response: HTTP %d", resp.StatusCode)

	if resp.StatusCode == http.StatusAccepted {
		return true, nil
	}

	body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	return false, fmt.Errorf("backend antwortete mit HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}

func splitIDMap(ids map[uint32]bool, maxPerChunk int) []map[uint32]bool {
	if maxPerChunk <= 0 {
		maxPerChunk = 20
	}

	list := make([]int, 0, len(ids))
	for id := range ids {
		list = append(list, int(id))
	}
	sort.Ints(list)

	var chunks []map[uint32]bool
	for start := 0; start < len(list); start += maxPerChunk {
		end := start + maxPerChunk
		if end > len(list) {
			end = len(list)
		}

		chunk := make(map[uint32]bool, end-start)
		for _, id := range list[start:end] {
			chunk[uint32(id)] = true
		}
		chunks = append(chunks, chunk)
	}

	return chunks
}

func evtSubscribe(channelPath, query string, signal windows.Handle, flags uint32) (windows.Handle, error) {
	chPtr, err := windows.UTF16PtrFromString(channelPath)
	if err != nil {
		return 0, err
	}
	qPtr, err := windows.UTF16PtrFromString(query)
	if err != nil {
		return 0, err
	}

	r1, _, e1 := procEvtSubscribe.Call(
		0, // Session = local
		uintptr(signal),
		uintptr(unsafe.Pointer(chPtr)),
		uintptr(unsafe.Pointer(qPtr)),
		0, // Bookmark
		0, // Context
		0, // Callback = NULL, weil Pull-Modell
		uintptr(flags),
	)
	if r1 == 0 {
		if e1 != syscall.Errno(0) {
			return 0, e1
		}
		return 0, errors.New("EvtSubscribe fehlgeschlagen")
	}

	return windows.Handle(r1), nil
}

func evtNext(resultSet windows.Handle, maxHandles uint32, timeout uint32) ([]windows.Handle, error) {
	handles := make([]windows.Handle, maxHandles)
	var returned uint32

	r1, _, e1 := procEvtNext.Call(
		uintptr(resultSet),
		uintptr(maxHandles),
		uintptr(unsafe.Pointer(&handles[0])),
		uintptr(timeout),
		0,
		uintptr(unsafe.Pointer(&returned)),
	)

	// r1 == 0 bedeutet, die Funktion war nicht erfolgreich (False)
	if r1 == 0 {
		// Wir prüfen, welcher Fehlercode vorliegt
		if e1 == windows.ERROR_NO_MORE_ITEMS || e1 == windows.ERROR_TIMEOUT {
			return nil, nil // Das ist kein Fehler, nur das Ende der Schlange
		}
		return nil, e1 // Ein echter Windows-Fehler
	}

	if returned == 0 {
		return nil, nil
	}

	return handles[:returned], nil
}

func evtRenderXML(eventHandle windows.Handle) (string, error) {
	var bufferUsed uint32
	var propertyCount uint32

	r1, _, e1 := procEvtRender.Call(
		0,
		uintptr(eventHandle),
		uintptr(evtRenderEventXML),
		0,
		0,
		uintptr(unsafe.Pointer(&bufferUsed)),
		uintptr(unsafe.Pointer(&propertyCount)),
	)

	if r1 == 0 {
		if errno, ok := e1.(syscall.Errno); ok {
			if errno != windows.ERROR_INSUFFICIENT_BUFFER {
				return "", errno
			}
		} else if e1 != syscall.Errno(0) {
			return "", e1
		}
	}

	if bufferUsed == 0 {
		return "", errors.New("EvtRender lieferte keine Buffergröße")
	}

	buf := make([]uint16, bufferUsed/2)

	r1, _, e1 = procEvtRender.Call(
		0,
		uintptr(eventHandle),
		uintptr(evtRenderEventXML),
		uintptr(bufferUsed),
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(unsafe.Pointer(&bufferUsed)),
		uintptr(unsafe.Pointer(&propertyCount)),
	)
	if r1 == 0 {
		if e1 != syscall.Errno(0) {
			return "", e1
		}
		return "", errors.New("EvtRender fehlgeschlagen")
	}

	return windows.UTF16ToString(buf), nil
}

func evtClose(h windows.Handle) error {
	r1, _, e1 := procEvtClose.Call(uintptr(h))
	if r1 == 0 {
		if e1 != syscall.Errno(0) {
			return e1
		}
		return errors.New("EvtClose fehlgeschlagen")
	}
	return nil
}

func installService() error {
	exepath, err := os.Executable()
	if err != nil {
		return err
	}

	m, err := mgr.Connect()
	if err != nil {
		return err
	}
	defer m.Disconnect()

	s, err := m.OpenService(ServiceName)
	if err == nil {
		s.Close()
		return fmt.Errorf("Dienst %q existiert bereits", ServiceName)
	}

	s, err = m.CreateService(ServiceName, exepath, mgr.Config{
		DisplayName: ServiceName,
		StartType:   mgr.StartAutomatic,
		Description: "Forwardet Windows Eventlogs an ein HTTP-Backend",
	})
	if err != nil {
		return err
	}
	defer s.Close()

	err = eventlog.InstallAsEventCreate(ServiceName, eventlog.Info|eventlog.Warning|eventlog.Error)
	if err != nil {
		_ = s.Delete()
		return err
	}

	return nil
}

func removeService() error {
	m, err := mgr.Connect()
	if err != nil {
		return err
	}
	defer m.Disconnect()

	s, err := m.OpenService(ServiceName)
	if err != nil {
		return err
	}
	defer s.Close()

	if err := s.Delete(); err != nil {
		return err
	}

	_ = eventlog.Remove(ServiceName)
	return nil
}

func loadAgentState(path string) (*AgentState, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return &AgentState{}, nil
		}
		return nil, err
	}

	var st AgentState
	if err := json.Unmarshal(data, &st); err != nil {
		return nil, err
	}
	return &st, nil
}

func saveAgentState(path string, st *AgentState) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}

	data, err := json.MarshalIndent(st, "", "  ")
	if err != nil {
		return err
	}

	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o600); err != nil {
		return err
	}

	return os.Rename(tmp, path)
}

func generateAgentKey() (string, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buf), nil
}

func loadAgentConfig(path string) (*AgentConfig, error) {
	cfg, err := ensureAgentConfig(path)
	if err != nil {
		return nil, err
	}

	if strings.TrimSpace(cfg.BackendURL) == "" {
		return nil, errors.New("backend_url fehlt")
	}
	if strings.TrimSpace(cfg.StateFile) == "" {
		return nil, errors.New("state_file fehlt")
	}

	return cfg, nil
}

func initAgent(configPath string) (*AgentConfig, *AgentState, string, error) {
	cfg, err := loadAgentConfig(configPath)
	if err != nil {
		return nil, nil, "", err
	}

	hostname, err := os.Hostname()
	if err != nil {
		return nil, nil, "", err
	}

	state, err := loadAgentState(cfg.StateFile)
	if err != nil {
		return nil, nil, "", err
	}

	if strings.TrimSpace(state.Hostname) == "" {
		state.Hostname = hostname
	}

	if strings.TrimSpace(state.APIKey) == "" {
		key, err := generateAgentKey()
		if err != nil {
			return nil, nil, "", err
		}
		state.APIKey = key
	}

	if err := saveAgentState(cfg.StateFile, state); err != nil {
		return nil, nil, "", err
	}

	return cfg, state, hostname, nil
}

func defaultAgentConfig() *AgentConfig {
	return &AgentConfig{
		BackendURL:    getenvDefault("SIEM_BACKEND_URL", "http://10.10.5.220:8090/ingest"),
		EnrollmentKey: getenvDefault("SIEM_ENROLLMENT_KEY", "BITTE_SEHR_LANG_UND_ZUFAELLIG"),
		StateFile:     `C:\ProgramData\WinEventForwarder\state.json`,
		ChannelRules: []ChannelRule{

			// =========================
			// SYSTEM
			// =========================
			{
				Name: "System",
				IDs: []uint32{
					1074, // planned shutdown
					6005, // startup
					6006, // shutdown
					6008, // unexpected shutdown
					7045, // service installed
				},
			},

			// =========================
			// SECURITY
			// =========================
			{
				Name: "Security",
				IDs: []uint32{

					// --- Logon / Auth ---
					4624, // logon success
					4625, // logon failed
					4648, // explicit credentials
					4672, // special privileges
					4673, 4674,

					// --- Security / Audit ---
					1102, // log cleared
					4719, // audit policy
					4902, 4904, 4905, 4906, 4907, 4908, 4912,

					// --- Time ---
					4616, // system time changed

					// --- User ---
					4720, 4722, 4723, 4724, 4725, 4726,
					4738,
					4740,

					// --- Groups ---
					4727, 4728, 4729,
					4730, 4731, 4732, 4733, 4734, 4735, 4737,
					4754, 4755, 4756, 4757, 4758,

					// --- Computer ---
					4741, 4742, 4743,

					// --- Kerberos / NTLM ---
					4768, 4769,
					4771, 4776,

					// --- Services / Tasks ---
					4697,
					4698, 4699,
					4700, 4701, 4702,

					// --- AD / Directory ---
					4662, // object access
					4670, // permission change
					5136, 5137, 5141,

					// --- Shares ---
					5140, 5145,

					// --- AD CS ---
					4882, 4885, 4886, 4887,
					4890, 4891, 4892,
					4898, 4899, 4900,

					// --- Defender ---
					5001, // real-time protection disabled
				},
			},

			// =========================
			// POWERSHELL
			// =========================
			{
				Name: "Microsoft-Windows-PowerShell/Operational",
				IDs: []uint32{
					4104, // script block
				},
			},
			{
				Name: "Windows PowerShell",
				IDs: []uint32{
					4104,
				},
			},

			// =========================
			// DEFENDER
			// =========================
			{
				Name: "Microsoft-Windows-Windows Defender/Operational",
				IDs: []uint32{
					1116, // malware detected
					1117, // remediation
					1118, 1119,
					5007, // config change
					5013,
				},
			},

			// =========================
			// WMI (Lateral Movement!)
			// =========================
			{
				Name: "Microsoft-Windows-WMI-Activity/Operational",
				IDs: []uint32{
					5857, 5858, 5859, 5860, 5861,
				},
			},

			// =========================
			// RDP
			// =========================
			{
				Name: "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational",
				IDs: []uint32{
					1149, // RDP login
				},
			},

			// =========================
			// OPTIONAL (laut!)
			// =========================

			/*
				{
					Name: "Security",
					IDs: []uint32{
						4688, // process creation (SEHR LAUT!)
					},
				},
			*/

		},
	}
}

func ensureAgentConfig(path string) (*AgentConfig, error) {
	data, err := os.ReadFile(path)
	if err == nil {
		var cfg AgentConfig
		if err := json.Unmarshal(data, &cfg); err != nil {
			return nil, fmt.Errorf("config ungültig: %w", err)
		}
		return &cfg, nil
	}

	if !errors.Is(err, os.ErrNotExist) {
		return nil, err
	}

	cfg := defaultAgentConfig()

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return nil, err
	}

	raw, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return nil, err
	}

	if err := os.WriteFile(path, raw, 0o600); err != nil {
		return nil, err
	}

	return cfg, nil
}

func getenvDefault(key, def string) string {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return def
	}
	return v
}

func buildChannelConfigs(rules []ChannelRule) []ChannelConfig {
	out := make([]ChannelConfig, 0, len(rules))
	for _, r := range rules {
		ids := make(map[uint32]bool, len(r.IDs))
		for _, id := range r.IDs {
			ids[id] = true
		}
		out = append(out, ChannelConfig{
			Name: r.Name,
			IDs:  ids,
		})
	}
	return out
}

func sendInitialHello(client *http.Client, cfg *AgentConfig, state *AgentState, hostname string) {
	batch := []LogPayload{
		{
			Hostname: hostname,
			Channel:  "Agent",
			EventID:  1,
			Source:   "WinEventForwarder",
			Time:     time.Now().UTC(),
			Message:  "Agent startup / enrollment hello",
		},
	}

	ok, err := sendBatch(client, cfg.BackendURL, state, cfg.EnrollmentKey, batch)
	if err != nil {
		log.Printf("Initial hello fehlgeschlagen: %v", err)
		return
	}

	if ok {
		if !state.Enrolled {
			state.Enrolled = true
			state.EnrolledAt = time.Now().UTC()

			if err := saveAgentState(cfg.StateFile, state); err != nil {
				log.Printf("state speichern nach Initial-Hello fehlgeschlagen: %v", err)
			} else {
				log.Printf("Agent erfolgreich per Initial-Hello enrolled")
			}
		}

		log.Printf("Initial hello erfolgreich gesendet")
	}
}

func setupFileLogging() error {
	logDir := `C:\ProgramData\WinEventForwarder`
	if err := os.MkdirAll(logDir, 0o755); err != nil {
		return err
	}

	f, err := os.OpenFile(
		filepath.Join(logDir, "agent.log"),
		os.O_CREATE|os.O_APPEND|os.O_WRONLY,
		0o644,
	)
	if err != nil {
		return err
	}

	log.SetOutput(f)
	log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.LUTC)
	return nil
}
