package ws

import (
	"encoding/json"
	"fmt"
	"log"
	"os/exec"
	"strings"
	"time"
)

type Message struct {
	Type    string                 `json:"type"`
	ID      string                 `json:"id,omitempty"`
	Command string                 `json:"command,omitempty"`
	Status  string                 `json:"status,omitempty"`
	Error   string                 `json:"error,omitempty"`
	Params  map[string]interface{} `json:"params,omitempty"`  // Backend -> Agent
	Data    interface{}            `json:"data,omitempty"`    // Agent -> Backend (Response)
}

type Handler struct {
	wingetPath string
}

func NewHandler() *Handler {
	return &Handler{}
}

// winget gibt den Pfad zu winget.exe zurueck (cached)
func (h *Handler) winget() string {
	if h.wingetPath != "" {
		return h.wingetPath
	}
	// Direkt im PATH?
	if path, err := exec.LookPath("winget"); err == nil {
		h.wingetPath = path
		return path
	}
	// WindowsApps durchsuchen
	script := `Get-ChildItem "C:\Program Files\WindowsApps" -Filter winget.exe -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName`
	cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script)
	out, err := cmd.Output()
	if err == nil {
		path := strings.TrimSpace(string(out))
		if path != "" {
			h.wingetPath = path
			log.Printf("winget gefunden: %s", path)
			return path
		}
	}
	log.Printf("winget nicht gefunden")
	return ""
}

func (h *Handler) Handle(raw []byte) []byte {
	var msg Message
	if err := json.Unmarshal(raw, &msg); err != nil {
		log.Printf("WS Parse-Fehler: %v", err)
		return nil
	}
	if msg.Type != "command" {
		return nil
	}

	var resp Message
	switch msg.Command {
	case "exec":
		resp = h.handleExec(msg)
	case "winget_list":
		resp = h.handleWingetList(msg)
	case "winget_install":
		resp = h.handleWingetInstall(msg)
	case "winget_upgrade":
		resp = h.handleWingetUpgrade(msg)
	case "winget_upgrade_all":
		resp = h.handleWingetUpgradeAll(msg)
	case "windows_updates_check":
		resp = h.handleWinUpdatesCheck(msg)
	case "windows_updates_install":
		resp = h.handleWinUpdatesInstall(msg)
	case "reboot":
		resp = h.handleReboot(msg)
	default:
		resp = Message{Type: "response", ID: msg.ID, Status: "error",
			Error: fmt.Sprintf("Unbekanntes Command: %s", msg.Command)}
	}

	out, _ := json.Marshal(resp)
	return out
}

// handleExec führt einen beliebigen Befehl aus (PowerShell oder cmd)
func (h *Handler) handleExec(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}

	data := msg.Params
	if data == nil {
		data, _ = msg.Data.(map[string]interface{})
	}
	command, _ := data["command"].(string)
	if command == "" {
		resp.Status = "error"
		resp.Error = "Kein Befehl angegeben"
		return resp
	}

	timeoutSecs := 60
	if t, ok := data["timeout"].(float64); ok && t > 0 {
		timeoutSecs = int(t)
	}

	out, err := runWithTimeout(command, timeoutSecs)
	if err != nil {
		resp.Status = "error"
		resp.Error = err.Error()
		resp.Data = map[string]interface{}{"output": out}
	} else {
		resp.Status = "ok"
		resp.Data = map[string]interface{}{"output": out}
	}
	return resp
}

// handleWingetList listet installierte Software auf
func (h *Handler) handleWingetList(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}
	wp := h.winget()
	if wp == "" {
		resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
	}
	out, err := runWithTimeout(fmt.Sprintf(`& "%s" list --accept-source-agreements 2>&1`, wp), 60)
	if err != nil && out == "" {
		resp.Status = "error"; resp.Error = err.Error(); return resp
	}
	resp.Status = "ok"
	resp.Data = map[string]interface{}{"output": out}
	return resp
}

// handleWingetInstall installiert ein Paket
func (h *Handler) handleWingetInstall(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}
	data := msg.Params
	if data == nil { data, _ = msg.Data.(map[string]interface{}) }
	pkg, _ := data["package"].(string)
	if pkg == "" {
		resp.Status = "error"; resp.Error = "Kein Paket angegeben"; return resp
	}
	wp := h.winget()
	if wp == "" {
		resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
	}
	cmd := fmt.Sprintf(`& "%s" install --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1`, wp, sanitize(pkg))
	out, err := runWithTimeout(cmd, 300)
	if err != nil {
		resp.Status = "error"
		resp.Error = err.Error()
		resp.Data = map[string]interface{}{"output": out}
	} else {
		resp.Status = "ok"
		resp.Data = map[string]interface{}{"output": out}
	}
	return resp
}

// handleWingetUpgrade aktualisiert ein Paket
func (h *Handler) handleWingetUpgrade(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}
	data := msg.Params
	if data == nil { data, _ = msg.Data.(map[string]interface{}) }
	pkg, _ := data["package"].(string)
	if pkg == "" {
		resp.Status = "error"; resp.Error = "Kein Paket angegeben"; return resp
	}
	wp := h.winget()
	if wp == "" {
		resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
	}
	cmd := fmt.Sprintf(`& "%s" upgrade --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1`, wp, sanitize(pkg))
	out, err := runWithTimeout(cmd, 300)
	if err != nil {
		resp.Status = "error"
		resp.Error = err.Error()
	} else {
		resp.Status = "ok"
		resp.Data = map[string]interface{}{"output": out}
	}
	return resp
}

// handleWingetUpgradeAll aktualisiert alle Pakete
func (h *Handler) handleWingetUpgradeAll(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}
	wp := h.winget()
	if wp == "" {
		resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
	}
	out, err := runWithTimeout(fmt.Sprintf(`& "%s" upgrade --all --silent --accept-package-agreements --accept-source-agreements 2>&1`, wp), 600)
	if err != nil {
		resp.Status = "error"
		resp.Error = err.Error()
	} else {
		resp.Status = "ok"
		resp.Data = map[string]interface{}{"output": out}
	}
	return resp
}

// handleWinUpdatesCheck prüft verfügbare Windows-Updates
func (h *Handler) handleWinUpdatesCheck(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}
	script := `
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searcher = $updateSession.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0 and Type='Software'")
$updates = @()
foreach ($u in $result.Updates) {
    $updates += [PSCustomObject]@{
        Title = $u.Title
        KB    = ($u.KBArticleIDs | Select-Object -First 1)
        Size  = $u.MaxDownloadSize
    }
}
$updates | ConvertTo-Json -Compress`
	out, err := runPSScript(script, 120)
	if err != nil {
		resp.Status = "error"
		resp.Error = err.Error()
		return resp
	}
	resp.Status = "ok"
	resp.Data = map[string]interface{}{"raw": out}
	return resp
}

// handleWinUpdatesInstall installiert alle ausstehenden Updates
func (h *Handler) handleWinUpdatesInstall(msg Message) Message {
	resp := Message{Type: "response", ID: msg.ID}
	script := `
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searcher = $updateSession.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0 and Type='Software'")
if ($result.Updates.Count -eq 0) { Write-Output "Keine Updates verfuegbar"; exit 0 }
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $result.Updates
$downloader.Download()
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $result.Updates
$installResult = $installer.Install()
Write-Output "Installiert: $($result.Updates.Count) Updates, ResultCode: $($installResult.ResultCode)"`
	out, err := runPSScript(script, 1800) // 30 Min Timeout
	if err != nil {
		resp.Status = "error"
		resp.Error = err.Error()
		resp.Data = map[string]interface{}{"output": out}
	} else {
		resp.Status = "ok"
		resp.Data = map[string]interface{}{"output": out, "reboot_required": strings.Contains(out, "ResultCode: 2")}
	}
	return resp
}

func (h *Handler) handleReboot(msg Message) Message {
	go func() {
		time.Sleep(3 * time.Second)
		exec.Command("shutdown", "/r", "/t", "10", "/c", "RMM-gesteuert").Run()
	}()
	return Message{Type: "response", ID: msg.ID, Status: "ok",
		Data: map[string]interface{}{"message": "Neustart in 10 Sekunden"}}
}

// Hilfsfunktionen

func runWithTimeout(command string, timeoutSecs int) (string, error) {
	cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command)
	out, err := withTimeout(cmd, time.Duration(timeoutSecs)*time.Second)
	return out, err
}

func runPS(command string) (string, error) {
	cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command)
	b, err := cmd.CombinedOutput()
	return strings.TrimSpace(string(b)), err
}

func runPSScript(script string, timeoutSecs int) (string, error) {
	cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script)
	out, err := withTimeout(cmd, time.Duration(timeoutSecs)*time.Second)
	return out, err
}

func withTimeout(cmd *exec.Cmd, timeout time.Duration) (string, error) {
	done := make(chan struct{})
	var out []byte
	var err error
	go func() {
		out, err = cmd.CombinedOutput()
		close(done)
	}()
	select {
	case <-done:
		return strings.TrimSpace(string(out)), err
	case <-time.After(timeout):
		cmd.Process.Kill()
		return "", fmt.Errorf("Timeout nach %v", timeout)
	}
}

func sanitize(s string) string {
	// Einfache Bereinigung — nur alphanumerisch, Punkt, Bindestrich
	var b strings.Builder
	for _, r := range s {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
			(r >= '0' && r <= '9') || r == '.' || r == '-' || r == '_' {
			b.WriteRune(r)
		}
	}
	return b.String()
}
