//go:build windows

package main

import (
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"time"

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

	"github.com/cynfo/rmm-agent-windows/client"
	"github.com/cynfo/rmm-agent-windows/collector"
	"github.com/cynfo/rmm-agent-windows/config"
	"github.com/cynfo/rmm-agent-windows/ws"
)

var Version = "1.0.0"

const (
	ServiceName        = "RMMAgent"
	ServiceDisplayName = "RMM Agent"
	ServiceDescription = "RMM Agent - Remote Monitoring & Management"
	AgentIDFile        = "agent_id.txt"
)

// agentService implementiert das Windows Service Interface
type agentService struct {
	cfg     *config.Config
	cfgPath string
}

func (s *agentService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
	changes <- svc.Status{State: svc.StartPending}
	stop := make(chan struct{})
	go runAgent(s.cfg, s.cfgPath, stop)
	changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}

	for c := range r {
		switch c.Cmd {
		case svc.Stop, svc.Shutdown:
			changes <- svc.Status{State: svc.StopPending}
			close(stop)
			return false, 0
		}
	}
	return false, 0
}

func main() {
	var (
		cfgPath   = flag.String("config", defaultConfigPath(), "Pfad zur Konfigurationsdatei")
		flagDebug = flag.Bool("debug", false, "Im Vordergrund ausfuehren (kein Dienst)")
		flagInst  = flag.Bool("install", false, "Als Windows-Dienst installieren")
		flagUninst = flag.Bool("uninstall", false, "Windows-Dienst deinstallieren")
	)
	flag.Parse()

	// Logging in Datei (neben Binary)
	logPath := filepath.Join(filepath.Dir(*cfgPath), "rmm-agent.log")
	if lf, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
		log.SetOutput(lf)
	}
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.Printf("RMM Windows Agent v%s startet", Version)

	cfg, err := config.Load(*cfgPath)
	if err != nil {
		log.Fatalf("Konfiguration laden fehlgeschlagen (%s): %v", *cfgPath, err)
	}

	if *flagInst {
		installService(cfg, *cfgPath)
		return
	}
	if *flagUninst {
		uninstallService()
		return
	}

	// Interaktiv (debug) oder als Dienst?
	isInteractive, err := svc.IsAnInteractiveSession()
	if err != nil {
		log.Printf("IsAnInteractiveSession: %v", err)
		isInteractive = true
	}

	if *flagDebug || isInteractive {
		log.Println("Starte im Debug-Modus (Vordergrund)")
		stop := make(chan struct{})
		runAgent(cfg, *cfgPath, stop)
	} else {
		if err := svc.Run(ServiceName, &agentService{cfg: cfg, cfgPath: *cfgPath}); err != nil {
			log.Fatalf("Dienst-Fehler: %v", err)
		}
	}
}

// runAgent ist die Hauptschleife des Agents
func runAgent(cfg *config.Config, cfgPath string, stop <-chan struct{}) {
	c := client.New(cfg.BackendURL, cfg.APIKey, cfg.Insecure)

	// Agent-ID laden oder registrieren
	agentID, err := loadOrRegister(c, cfg, cfgPath)
	if err != nil {
		log.Fatalf("Registrierung fehlgeschlagen: %v", err)
	}
	log.Printf("Agent-ID: %s", agentID)

	// WS-Handler und -Client starten
	handler := ws.NewHandler()
	wsClient := ws.NewClient(cfg.BackendURL, agentID, cfg.APIKey, cfg.Insecure, handler)
	go wsClient.Run(stop)

	// Heartbeat-Schleife
	interval := time.Duration(cfg.IntervalSeconds) * time.Second
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	// Ersten Heartbeat sofort
	sendHeartbeat(c, cfg, agentID)

	for {
		select {
		case <-stop:
			log.Println("Agent wird gestoppt")
			return
		case <-ticker.C:
			sendHeartbeat(c, cfg, agentID)
		}
	}
}

func sendHeartbeat(c *client.Client, cfg *config.Config, agentID string) {
	sysData, err := collector.Collect()
	if err != nil {
		log.Printf("Collector-Fehler: %v", err)
	}

	var rawData json.RawMessage
	if sysData != nil {
		rawData, _ = sysData.ToJSON()
	}

	resp, err := c.Heartbeat(client.HeartbeatRequest{
		AgentID:      agentID,
		AgentVersion: Version,
		Platform:     "windows",
		SystemData:   rawData,
	})
	if err != nil {
		log.Printf("Heartbeat fehlgeschlagen: %v", err)
		return
	}
	log.Printf("Heartbeat OK")

	if resp.UpdateAvailable && resp.UpdateVersion != "" && resp.UpdateVersion != Version {
		log.Printf("Update verfuegbar: v%s → v%s", Version, resp.UpdateVersion)
		go doSelfUpdate(cfg, resp.UpdateVersion, resp.UpdateHash)
	}
}

// doSelfUpdate laedt das neue Binary herunter und startet einen
// PowerShell-Script der nach Dienst-Stop das Binary tauscht und neu startet.
func doSelfUpdate(cfg *config.Config, newVersion, expectedHash string) {
	log.Printf("OTA-Update: Starte Download v%s", newVersion)

	exe, err := os.Executable()
	if err != nil {
		log.Printf("OTA-Update: Executable-Pfad: %v", err)
		return
	}
	dir := filepath.Dir(exe)
	tmpBin := filepath.Join(dir, "rmm-agent-windows-update.exe")
	scriptPath := filepath.Join(dir, "rmm-update.ps1")

	// Binary herunterladen
	dlURL := fmt.Sprintf("%s/api/v1/firmware/download?platform=windows&api_key=%s",
		cfg.BackendURL, cfg.APIKey)

	tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.Insecure}}
	hc := &http.Client{Transport: tr, Timeout: 10 * time.Minute}

	req, err := http.NewRequest("GET", dlURL, nil)
	if err != nil {
		log.Printf("OTA-Update: Request: %v", err)
		return
	}
	req.Header.Set("X-API-Key", cfg.APIKey)

	resp, err := hc.Do(req)
	if err != nil {
		log.Printf("OTA-Update: Download: %v", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Printf("OTA-Update: HTTP %d", resp.StatusCode)
		return
	}

	f, err := os.Create(tmpBin)
	if err != nil {
		log.Printf("OTA-Update: Temp-Datei: %v", err)
		return
	}

	h := sha256.New()
	if _, err := io.Copy(io.MultiWriter(f, h), resp.Body); err != nil {
		f.Close()
		os.Remove(tmpBin)
		log.Printf("OTA-Update: Schreiben: %v", err)
		return
	}
	f.Close()

	// Hash pruefen
	gotHash := hex.EncodeToString(h.Sum(nil))
	if expectedHash != "" && gotHash != expectedHash {
		os.Remove(tmpBin)
		log.Printf("OTA-Update: Hash-Mismatch: erwartet=%s, erhalten=%s", expectedHash, gotHash)
		return
	}

	log.Printf("OTA-Update: Download OK (hash=%s), starte Update-Script", gotHash[:12])

	// PowerShell-Script schreiben das nach Dienst-Stop das Binary tauscht
	psScript := fmt.Sprintf(`
Start-Sleep -Seconds 3
Stop-Service -Name RMMAgent -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Copy-Item -Path "%s" -Destination "%s" -Force
Remove-Item -Path "%s" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "%s" -Force -ErrorAction SilentlyContinue
Start-Service -Name RMMAgent
`, tmpBin, exe, tmpBin, scriptPath)

	if err := os.WriteFile(scriptPath, []byte(psScript), 0644); err != nil {
		log.Printf("OTA-Update: Script schreiben: %v", err)
		return
	}

	// Script detached starten (laeuft weiter wenn dieser Prozess beendet wird)
	cmd := exec.Command("powershell", "-NonInteractive", "-ExecutionPolicy", "Bypass",
		"-WindowStyle", "Hidden", "-File", scriptPath)
	if err := cmd.Start(); err != nil {
		log.Printf("OTA-Update: Script starten: %v", err)
		return
	}
	// cmd.Wait() nicht aufrufen — Script laeuft unabhaengig weiter
	log.Printf("OTA-Update: Script gestartet (PID %d), Dienst wird gestoppt", cmd.Process.Pid)
}

// loadOrRegister liest die Agent-ID aus Datei oder registriert sich neu
func loadOrRegister(c *client.Client, cfg *config.Config, cfgPath string) (string, error) {
	idFile := filepath.Join(filepath.Dir(cfgPath), AgentIDFile)

	if data, err := os.ReadFile(idFile); err == nil {
		id := string(data)
		if len(id) > 0 {
			// Re-registrieren um sicherzustellen dass der Agent im Backend bekannt ist
			hostname, _ := os.Hostname()
			c.Register(client.RegisterRequest{
				AgentID:      id,
				Name:         cfg.AgentName,
				Hostname:     hostname,
				IP:           "",
				AgentVersion: Version,
				Platform:     "windows",
			})
			return id, nil
		}
	}

	// Neu registrieren
	hostname, _ := os.Hostname()
	name := cfg.AgentName
	if name == "" {
		name = hostname
	}
	id, err := c.Register(client.RegisterRequest{
		Name:         name,
		Hostname:     hostname,
		AgentVersion: Version,
		Platform:     "windows",
	})
	if err != nil {
		return "", fmt.Errorf("Registrierung: %w", err)
	}

	if err := os.WriteFile(idFile, []byte(id), 0644); err != nil {
		log.Printf("Agent-ID speichern fehlgeschlagen: %v", err)
	}
	log.Printf("Neu registriert als: %s", id)
	return id, nil
}

// defaultConfigPath gibt den Standardpfad zur Konfigurationsdatei zurück
func defaultConfigPath() string {
	exe, err := os.Executable()
	if err != nil {
		return `C:\Program Files\RMMAgent\config.yaml`
	}
	return filepath.Join(filepath.Dir(exe), "config.yaml")
}

// installService installiert den Agent als Windows-Dienst
func installService(cfg *config.Config, cfgPath string) {
	m, err := mgr.Connect()
	if err != nil {
		log.Fatalf("Service Manager: %v", err)
	}
	defer m.Disconnect()

	s, err := m.OpenService(ServiceName)
	if err == nil {
		s.Close()
		log.Fatalf("Dienst '%s' existiert bereits — zuerst deinstallieren", ServiceName)
	}

	exe, err := os.Executable()
	if err != nil {
		log.Fatalf("Executable-Pfad: %v", err)
	}

	s, err = m.CreateService(ServiceName, exe, mgr.Config{
		DisplayName:  ServiceDisplayName,
		Description:  ServiceDescription,
		StartType:    mgr.StartAutomatic,
		ErrorControl: mgr.ErrorNormal,
	}, "-config", cfgPath)
	if err != nil {
		log.Fatalf("Dienst erstellen: %v", err)
	}
	defer s.Close()

	// Event Log registrieren
	eventlog.InstallAsEventCreate(ServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)

	log.Printf("Dienst '%s' installiert", ServiceName)
	log.Println("Starten mit: net start RMMAgent")
}

// uninstallService entfernt den Windows-Dienst
func uninstallService() {
	m, err := mgr.Connect()
	if err != nil {
		log.Fatalf("Service Manager: %v", err)
	}
	defer m.Disconnect()

	s, err := m.OpenService(ServiceName)
	if err != nil {
		log.Fatalf("Dienst nicht gefunden: %v", err)
	}
	defer s.Close()

	s.Control(svc.Stop)
	time.Sleep(2 * time.Second)

	if err := s.Delete(); err != nil {
		log.Fatalf("Dienst loeschen: %v", err)
	}

	eventlog.Remove(ServiceName)
	log.Printf("Dienst '%s' deinstalliert", ServiceName)
}
