package ws

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"
)

type Handler struct {
	client *Client
}

func NewHandler(client *Client) *Handler {
	return &Handler{client: client}
}

func (h *Handler) HandleCommand(msg Message) {
	log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID)

	var response Message
	response.Type = "response"
	response.ID = msg.ID

	switch msg.Command {
	case "exec":
		response = h.handleExec(msg)
	case "update":
		response = h.handleUpdate(msg)
	case "major_update":
		response = h.handleMajorUpdate(msg)
	case "update_check":
		response = h.handleUpdateCheck(msg)
	case "reboot":
		response = h.handleReboot(msg)
	case "backup":
		response = h.handleBackup(msg)
	case "wg_add_peer":
		response = h.handleWGAddPeer(msg)
	case "wg_delete_peer":
		response = h.handleWGDeletePeer(msg)
	case "wg_list_peers":
		response = h.handleWGListPeers(msg)
	case "tunnel_connect":
		response = h.handleTunnelConnect(msg)
	case "tunnel_disconnect":
		h.handleTunnelDisconnect(msg)
		return
	case "tunnel_open":
		response.Status = "ok"
		response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
	case "tunnel_close":
		response.Status = "ok"
	case "pty_start":
		response = h.handlePTYStart(msg)
	case "pty_stop":
		h.handlePTYStop(msg)
		return
	case "pty_resize":
		h.handlePTYResize(msg)
		return
	case "agent_update":
		response = h.handleAgentUpdate(msg)
	default:
		response.Status = "error"
		response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
	}

	if err := h.client.SendMessage(response); err != nil {
		log.Printf("Response senden fehlgeschlagen: %v", err)
	}
}

// handleExec fuehrt einen beliebigen Befehl aus
func (h *Handler) handleExec(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	command, ok := msg.Params["command"].(string)
	if !ok || command == "" {
		response.Status = "error"
		response.Error = "Parameter 'command' erforderlich"
		return response
	}

	timeoutSec := 30
	if t, ok := msg.Params["timeout"]; ok {
		if ts, ok := t.(float64); ok {
			timeoutSec = int(ts)
		} else if ts, ok := t.(string); ok {
			if parsed, err := strconv.Atoi(ts); err == nil {
				timeoutSec = parsed
			}
		}
	}

	output, err := h.runCommand(command, timeoutSec)

	if err != nil {
		response.Status = "error"
		response.Error = err.Error()
		response.Data = map[string]interface{}{"output": output}
	} else {
		response.Status = "ok"
		response.Data = map[string]interface{}{"output": output}
	}

	return response
}

// handleUpdateCheck prueft ob Updates verfuegbar sind
func (h *Handler) handleUpdateCheck(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}
	log.Println("Update-Check gestartet...")

	result := map[string]interface{}{}

	// 1. OPNsense Core Update pruefen
	// opnsense-update -c gibt Exit 1 wenn Updates da sind, aber auch bei anderen Fehlern.
	// Nur wenn stdout tatsaechlich Update-Infos enthaelt, ist ein Update verfuegbar.
	coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60)
	coreInfo := strings.TrimSpace(coreOutput)
	if coreErr != nil && coreInfo != "" {
		// Exit 1 MIT Output = echtes Update verfuegbar
		result["core_update_available"] = true
		result["core_update_info"] = coreInfo
	} else {
		result["core_update_available"] = false
		result["core_update_info"] = "Kein Core-Update verfuegbar"
	}

	// Reboot-Check: Base/Kernel Version != installierte Version?
	baseVer, _ := h.runCommand("cat /usr/local/opnsense/version/base", 5)
	kernelVer, _ := h.runCommand("cat /usr/local/opnsense/version/kernel", 5)
	coreJson, _ := h.runCommand("cat /usr/local/opnsense/version/core", 5)
	baseVer = strings.TrimSpace(baseVer)
	kernelVer = strings.TrimSpace(kernelVer)

	rebootRequired := false
	// Pruefen ob CORE_SERIES sich von Base/Kernel unterscheidet
	if strings.Contains(coreJson, "CORE_NEXT") && baseVer != "" && kernelVer != "" {
		// Einfacher Check: wenn base/kernel neuer als core → reboot noetig
		coreVer := ""
		for _, line := range strings.Split(coreJson, "\n") {
			if strings.Contains(line, "CORE_PKGVERSION") {
				parts := strings.SplitN(line, ":", 2)
				if len(parts) == 2 {
					coreVer = strings.Trim(strings.TrimSpace(parts[1]), "\",")
				}
			}
		}
		if coreVer != "" && baseVer != "" && !strings.HasPrefix(baseVer, strings.Split(coreVer, ".")[0]+"."+strings.Split(coreVer, ".")[1]) {
			// Base-Version Major.Minor unterscheidet sich von Core → Reboot
			rebootRequired = true
		}
	}
	result["reboot_required"] = rebootRequired

	// 2. Paket-Updates pruefen
	pkgOutput, _ := h.runCommand("/usr/sbin/pkg upgrade -n", 60)
	pendingPackages := h.parsePkgUpgrade(pkgOutput)
	result["package_update_available"] = len(pendingPackages) > 0
	result["pending_packages"] = pendingPackages
	result["pending_count"] = len(pendingPackages)

	response.Status = "ok"
	response.Data = result
	log.Printf("Update-Check abgeschlossen: Core=%v, Packages=%d",
		result["core_update_available"], len(pendingPackages))
	return response
}

// handleUpdate fuehrt ein normales Update durch (Core + Packages)
func (h *Handler) handleUpdate(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}
	log.Println("Normales Update gestartet...")

	reboot := false
	if r, ok := msg.Params["reboot"].(bool); ok {
		reboot = r
	}

	result := map[string]interface{}{
		"steps": []map[string]interface{}{},
	}
	var steps []map[string]interface{}

	// Schritt 1: opnsense-update (Core + Firmware)
	log.Println("Update Schritt 1: opnsense-update")
	coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update", 600)
	step1 := map[string]interface{}{
		"step":   "opnsense-update",
		"output": coreOutput,
		"ok":     coreErr == nil,
	}
	if coreErr != nil {
		step1["error"] = coreErr.Error()
	}
	steps = append(steps, step1)

	// Schritt 2: pkg upgrade -y (Paket-Updates)
	log.Println("Update Schritt 2: pkg upgrade -y")
	pkgOutput, pkgErr := h.runCommand("/usr/sbin/pkg upgrade -y", 600)
	step2 := map[string]interface{}{
		"step":   "pkg upgrade",
		"output": pkgOutput,
		"ok":     pkgErr == nil,
	}
	if pkgErr != nil {
		step2["error"] = pkgErr.Error()
	}
	steps = append(steps, step2)

	// Schritt 3: opnsense-update -c (Nachpruefung)
	log.Println("Update Schritt 3: Nachpruefung")
	checkOutput, checkErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60)
	rebootRequired := checkErr != nil // Exit 1 nach Update = Reboot noetig
	step3 := map[string]interface{}{
		"step":            "verify",
		"output":          checkOutput,
		"reboot_required": rebootRequired,
	}
	steps = append(steps, step3)

	result["steps"] = steps
	result["reboot_required"] = rebootRequired
	result["reboot_requested"] = reboot

	// Gesamtstatus
	if coreErr == nil && pkgErr == nil {
		response.Status = "ok"
		if rebootRequired {
			result["message"] = "Update erfolgreich — Reboot erforderlich"
		} else {
			result["message"] = "Update erfolgreich abgeschlossen"
		}
	} else {
		response.Status = "ok"
		result["message"] = "Update mit Warnungen abgeschlossen"
	}

	// Reboot wenn gewuenscht
	if reboot {
		log.Println("Reboot nach Update angefordert...")
		result["reboot"] = "Reboot in 5 Sekunden..."
		response.Data = result

		// Response erst senden, dann rebooten
		go func() {
			time.Sleep(5 * time.Second)
			log.Println("Reboot wird ausgefuehrt...")
			exec.Command("/sbin/shutdown", "-r", "now").Run()
		}()

		return response
	}

	response.Data = result
	log.Println("Update abgeschlossen")
	return response
}

// handleMajorUpdate fuehrt ein Major-Upgrade durch
// Ablauf: Phase 1: opnsense-update -r <ver> -bkp → Reboot
//         Phase 2 (nach Reboot): pkg-static update -f → pkg-static upgrade -y → optionaler Reboot
// WICHTIG: Das neue Paket-Repository wird erst NACH dem Reboot aktiv,
// daher muss pkg upgrade zwingend nach dem Reboot laufen.
func (h *Handler) handleMajorUpdate(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	targetVersion, ok := msg.Params["version"].(string)
	if !ok || targetVersion == "" {
		response.Status = "error"
		response.Error = "Parameter 'version' erforderlich (z.B. '26.1')"
		return response
	}

	// Phase 2: Wenn phase=2, nur pkg upgrade ausfuehren (nach Reboot)
	phase := "1"
	if p, ok := msg.Params["phase"].(string); ok && p != "" {
		phase = p
	} else if p, ok := msg.Params["phase"].(float64); ok {
		phase = fmt.Sprintf("%.0f", p)
	}

	log.Printf("Major-Update auf Version %s, Phase %s gestartet...", targetVersion, phase)

	result := map[string]interface{}{
		"target_version": targetVersion,
		"phase":          phase,
	}
	var steps []map[string]interface{}
	hasError := false

	if phase == "2" {
		// Phase 2: Nach Reboot — Repo auf neue Version umstellen, dann pkg upgrade
		// Das OPNsense pkg-Repo wird vom opnsense Package kontrolliert (CORE_REPOSITORY in /usr/local/opnsense/version/core)
		// Nach opnsense-update -r <ver> -bkp + Reboot zeigt das Repo noch die alte Version
		// Wir muessen die Repo-Config manuell umschreiben
		repoConf := "/usr/local/etc/pkg/repos/OPNsense.conf"
		log.Printf("Phase 2: Repo-Config auf %s umstellen", targetVersion)
		// Aktuelle Series aus dem Repo-Config lesen und durch targetVersion ersetzen
		sedCmd := fmt.Sprintf("/usr/bin/sed -i '' 's|/[0-9][0-9]\\.[0-9]/latest|/%s/latest|g' %s", targetVersion, repoConf)
		sedOutput, sedErr := h.runCommand(sedCmd, 10)
		step0 := map[string]interface{}{
			"step":   "repo-switch to " + targetVersion,
			"output": sedOutput,
			"ok":     sedErr == nil,
		}
		if sedErr != nil {
			step0["error"] = sedErr.Error()
			hasError = true
		}
		steps = append(steps, step0)

		log.Println("Phase 2: pkg-static update -f")
		catalogOutput, catalogErr := h.runCommand("/usr/local/sbin/pkg-static update -f", 120)
		step1 := map[string]interface{}{
			"step":   "pkg-static update -f",
			"output": catalogOutput,
			"ok":     catalogErr == nil,
		}
		if catalogErr != nil {
			step1["error"] = catalogErr.Error()
			hasError = true
		}
		steps = append(steps, step1)

		log.Println("Phase 2: pkg-static upgrade -y")
		pkgOutput, pkgErr := h.runCommand("/usr/local/sbin/pkg-static upgrade -y", 900)
		step2 := map[string]interface{}{
			"step":   "pkg-static upgrade -y",
			"output": pkgOutput,
			"ok":     pkgErr == nil,
		}
		if pkgErr != nil {
			step2["error"] = pkgErr.Error()
			hasError = true
		}
		steps = append(steps, step2)

		// Verify
		verOutput, _ := h.runCommand("/usr/local/sbin/opnsense-version", 10)
		verStep := map[string]interface{}{
			"step":   "verify",
			"output": strings.TrimSpace(verOutput),
			"ok":     true,
		}
		steps = append(steps, verStep)

		result["steps"] = steps
		result["reboot_required"] = false

		if !hasError {
			result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s abgeschlossen — Packages aktualisiert", targetVersion)
		} else {
			result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s mit Fehlern", targetVersion)
		}

		response.Status = "ok"
		response.Data = result
		return response
	}

	// Phase 1: Base + Kernel installieren, dann Reboot
	reboot := true
	if r, ok := msg.Params["reboot"].(bool); ok {
		reboot = r
	}

	log.Printf("Phase 1: opnsense-update -r %s -bkp", targetVersion)
	releaseCmd := fmt.Sprintf("/usr/local/sbin/opnsense-update -r %s -bkp", targetVersion)
	releaseOutput, releaseErr := h.runCommand(releaseCmd, 900)
	step1 := map[string]interface{}{
		"step":   fmt.Sprintf("opnsense-update -r %s -bkp", targetVersion),
		"output": releaseOutput,
		"ok":     releaseErr == nil,
	}
	if releaseErr != nil {
		step1["error"] = releaseErr.Error()
		hasError = true
	}
	steps = append(steps, step1)

	result["steps"] = steps
	result["reboot_required"] = true
	result["reboot_requested"] = reboot

	if !hasError {
		result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s abgeschlossen — Reboot erforderlich, danach Phase 2 ausfuehren", targetVersion)
	} else {
		result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s mit Fehlern", targetVersion)
	}

	response.Status = "ok"

	if reboot {
		log.Println("Reboot nach Phase 1 angefordert...")
		result["reboot"] = "Reboot in 5 Sekunden..."
		response.Data = result

		go func() {
			time.Sleep(5 * time.Second)
			log.Println("Reboot wird ausgefuehrt...")
			exec.Command("/sbin/shutdown", "-r", "now").Run()
		}()

		return response
	}

	response.Data = result
	log.Printf("Major-Update Phase 1 auf %s abgeschlossen (Reboot noch ausstehend)", targetVersion)
	return response
}

// handleReboot fuehrt einen Reboot durch
func (h *Handler) handleReboot(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	delaySec := 5
	if d, ok := msg.Params["delay"].(float64); ok && d > 0 {
		delaySec = int(d)
	}

	log.Printf("Reboot angefordert (Verzoegerung: %ds)", delaySec)

	response.Status = "ok"
	response.Data = map[string]interface{}{
		"message": fmt.Sprintf("Reboot in %d Sekunden", delaySec),
	}

	go func() {
		time.Sleep(time.Duration(delaySec) * time.Second)
		log.Println("Reboot wird ausgefuehrt...")
		exec.Command("/sbin/shutdown", "-r", "now").Run()
	}()

	return response
}

// --- Backup Handler ---

// handleBackup liest die OPNsense config.xml und sendet sie zurueck
func (h *Handler) handleBackup(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	configPath := "/conf/config.xml"
	if p, ok := msg.Params["path"].(string); ok && p != "" {
		configPath = p
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Config lesen fehlgeschlagen: %v", err)
		return response
	}

	// SHA256-Hash berechnen
	hash := sha256.Sum256(data)
	hashStr := hex.EncodeToString(hash[:])

	// Base64-kodiert senden (config.xml ist Text, aber sicherstellen dass nichts verloren geht)
	encoded := base64.StdEncoding.EncodeToString(data)

	response.Status = "ok"
	response.Data = map[string]interface{}{
		"config":   encoded,
		"hash":     hashStr,
		"size":     len(data),
		"path":     configPath,
		"encoding": "base64",
	}

	log.Printf("Backup erstellt: %s (%d Bytes, Hash: %s)", configPath, len(data), hashStr[:12])
	return response
}

// --- Tunnel Handlers ---

func (h *Handler) handleTunnelConnect(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	sessionID, _ := msg.Params["session_id"].(string)
	tunnelID, _ := msg.Params["tunnel_id"].(string)
	targetHost, _ := msg.Params["target_host"].(string)
	targetPortFloat, _ := msg.Params["target_port"].(float64)
	targetPort := int(targetPortFloat)

	if sessionID == "" || targetHost == "" || targetPort <= 0 {
		response.Status = "error"
		response.Error = "session_id, target_host und target_port erforderlich"
		return response
	}

	if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err)
		return response
	}

	response.Status = "ok"
	response.Data = map[string]interface{}{"session_id": sessionID}
	return response
}

func (h *Handler) handleTunnelDisconnect(msg Message) {
	sessionID, _ := msg.Params["session_id"].(string)
	if sessionID != "" {
		h.client.tunnelManager.DisconnectSession(sessionID)
	}
}

// --- Helper ---

// runCommand fuehrt einen Shell-Befehl mit Timeout aus
func (h *Handler) runCommand(command string, timeoutSec int) (string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
	output, err := cmd.CombinedOutput()
	return string(output), err
}

// parsePkgUpgrade parst die Ausgabe von "pkg upgrade -n"
func (h *Handler) parsePkgUpgrade(output string) []map[string]string {
	var packages []map[string]string

	inUpgradeBlock := false
	for _, line := range strings.Split(output, "\n") {
		trimmed := strings.TrimSpace(line)

		if strings.Contains(trimmed, "packages to be UPGRADED") ||
			strings.Contains(trimmed, "will be affected") {
			inUpgradeBlock = true
			continue
		}

		if inUpgradeBlock && strings.Contains(trimmed, "->") {
			parts := strings.SplitN(trimmed, ":", 2)
			if len(parts) == 2 {
				pkg := strings.TrimSpace(parts[0])
				versions := strings.TrimSpace(parts[1])

				arrowParts := strings.SplitN(versions, "->", 2)
				if len(arrowParts) == 2 {
					packages = append(packages, map[string]string{
						"package":         pkg,
						"current_version": strings.TrimSpace(arrowParts[0]),
						"new_version":     strings.TrimSpace(arrowParts[1]),
					})
				}
			}
		}

		if inUpgradeBlock && (trimmed == "" || strings.HasPrefix(trimmed, "Number of")) {
			if !strings.HasPrefix(trimmed, "Number of") {
				inUpgradeBlock = false
			}
		}
	}

	return packages
}

func (h *Handler) handleAgentUpdate(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	// Binary kommt als Base64 im Data-Feld
	data, ok := msg.Data.(map[string]interface{})
	if !ok {
		response.Status = "error"
		response.Error = "Ungueltige Daten"
		return response
	}

	binaryB64, _ := data["binary"].(string)
	expectedHash, _ := data["hash"].(string)
	newVersion, _ := data["version"].(string)

	if binaryB64 == "" || expectedHash == "" {
		response.Status = "error"
		response.Error = "Binary oder Hash fehlt"
		return response
	}

	log.Printf("Agent-Update empfangen: Version %s, Hash %s", newVersion, expectedHash[:16])

	// Base64 dekodieren
	binary, err := base64.StdEncoding.DecodeString(binaryB64)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Base64-Dekodierung fehlgeschlagen: %v", err)
		return response
	}

	// Hash pruefen
	hash := sha256.Sum256(binary)
	actualHash := hex.EncodeToString(hash[:])
	if actualHash != expectedHash {
		response.Status = "error"
		response.Error = fmt.Sprintf("Hash-Mismatch: erwartet %s, bekommen %s", expectedHash[:16], actualHash[:16])
		return response
	}

	// Aktuelles Binary finden
	binaryPath := "/usr/local/rmm/rmm-agent"
	if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
		// Fallback: eigener Pfad
		binaryPath, _ = os.Executable()
	}

	// Backup vom alten Binary
	backupPath := binaryPath + ".old"
	if err := os.Rename(binaryPath, backupPath); err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Backup fehlgeschlagen: %v", err)
		return response
	}

	// Neues Binary schreiben
	if err := os.WriteFile(binaryPath, binary, 0755); err != nil {
		// Rollback
		os.Rename(backupPath, binaryPath)
		response.Status = "error"
		response.Error = fmt.Sprintf("Binary schreiben fehlgeschlagen: %v", err)
		return response
	}

	log.Printf("Neues Binary geschrieben: %s (%d bytes)", binaryPath, len(binary))

	response.Status = "ok"
	response.Data = map[string]interface{}{
		"message":     fmt.Sprintf("Update auf %s erfolgreich, Neustart...", newVersion),
		"old_version": data["old_version"],
		"new_version": newVersion,
	}

	// Response senden, dann Neustart
	go func() {
		time.Sleep(2 * time.Second)
		log.Printf("Agent-Neustart nach Update auf %s", newVersion)
		exec.Command("/usr/sbin/service", "rmm_agent", "restart").Run()
	}()

	return response
}

// SendSessionData schickt Tunnel-Daten als Binary-Message zurueck zum Backend
func (h *Handler) SendSessionData(sessionID string, data []byte) error {
	idBytes := []byte(sessionID)
	if len(idBytes) > 255 {
		return fmt.Errorf("session_id zu lang")
	}

	message := make([]byte, 1+len(idBytes)+len(data))
	message[0] = byte(len(idBytes))
	copy(message[1:], idBytes)
	copy(message[1+len(idBytes):], data)

	return h.client.SendBinaryData(message)
}

// --- PTY Handlers ---

func (h *Handler) handlePTYStart(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	sessionID, _ := msg.Params["session_id"].(string)
	if sessionID == "" {
		response.Status = "error"
		response.Error = "session_id erforderlich"
		return response
	}

	cols := uint16(80)
	rows := uint16(24)
	if c, ok := msg.Params["cols"].(float64); ok && c > 0 {
		cols = uint16(c)
	}
	if r, ok := msg.Params["rows"].(float64); ok && r > 0 {
		rows = uint16(r)
	}

	if err := h.client.ptyManager.StartPTY(sessionID, cols, rows); err != nil {
		response.Status = "error"
		response.Error = err.Error()
		return response
	}

	response.Status = "ok"
	response.Data = map[string]interface{}{"session_id": sessionID}
	return response
}

func (h *Handler) handlePTYStop(msg Message) {
	sessionID, _ := msg.Params["session_id"].(string)
	if sessionID != "" {
		h.client.ptyManager.StopPTY(sessionID)
	}
}

func (h *Handler) handlePTYResize(msg Message) {
	sessionID, _ := msg.Params["session_id"].(string)
	cols := uint16(80)
	rows := uint16(24)
	if c, ok := msg.Params["cols"].(float64); ok && c > 0 {
		cols = uint16(c)
	}
	if r, ok := msg.Params["rows"].(float64); ok && r > 0 {
		rows = uint16(r)
	}
	if sessionID != "" {
		h.client.ptyManager.ResizePTY(sessionID, cols, rows)
	}
}
