package ws

import (
	"encoding/xml"
	"fmt"
	"log"
	"net"
	"os"
	"strings"
)

// --- OPNsense config.xml Structs ---

type OPNsenseConfig struct {
	XMLName   xml.Name       `xml:"opnsense"`
	WireGuard *WGConfig      `xml:"OPNsense>wireguard"`
	Rest      []byte         `xml:",innerxml"`
}

type WGConfig struct {
	General WGGeneral `xml:"general"`
	Server  WGServer  `xml:"server"`
	Client  WGClient  `xml:"client"`
}

type WGGeneral struct {
	XMLName     xml.Name `xml:"general"`
	Version     string   `xml:"version,attr"`
	PersistedAt string   `xml:"persisted_at,attr"`
	Description string   `xml:"description,attr"`
	Enabled     string   `xml:"enabled"`
}

type WGServer struct {
	XMLName     xml.Name       `xml:"server"`
	Version     string         `xml:"version,attr"`
	PersistedAt string         `xml:"persisted_at,attr"`
	Description string         `xml:"description,attr"`
	Servers     []WGServerInst `xml:"servers>server"`
}

type WGServerInst struct {
	UUID          string `xml:"uuid,attr"`
	Enabled       string `xml:"enabled"`
	Name          string `xml:"name"`
	Instance      string `xml:"instance"`
	PubKey        string `xml:"pubkey"`
	PrivKey       string `xml:"privkey"`
	Port          string `xml:"port"`
	MTU           string `xml:"mtu"`
	DNS           string `xml:"dns"`
	TunnelAddress string `xml:"tunneladdress"`
	DisableRoutes string `xml:"disableroutes"`
	Gateway       string `xml:"gateway"`
	CarpDependOn  string `xml:"carp_depend_on"`
	Peers         string `xml:"peers"`
	Debug         string `xml:"debug"`
	Endpoint      string `xml:"endpoint"`
	PeerDNS       string `xml:"peer_dns"`
}

type WGClient struct {
	XMLName     xml.Name       `xml:"client"`
	Version     string         `xml:"version,attr"`
	PersistedAt string         `xml:"persisted_at,attr"`
	Description string         `xml:"description,attr"`
	Clients     []WGClientInst `xml:"clients>client"`
}

type WGClientInst struct {
	UUID          string `xml:"uuid,attr"`
	Enabled       string `xml:"enabled"`
	Name          string `xml:"name"`
	PubKey        string `xml:"pubkey"`
	PSK           string `xml:"psk"`
	TunnelAddress string `xml:"tunneladdress"`
	ServerAddress string `xml:"serveraddress"`
	ServerPort    string `xml:"serverport"`
	KeepAlive     string `xml:"keepalive"`
}

// handleWGAddPeer erzeugt einen neuen WireGuard-Peer
func (h *Handler) handleWGAddPeer(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	name, _ := msg.Params["name"].(string)
	allowedIPs, _ := msg.Params["allowed_ips"].(string)
	if name == "" {
		response.Status = "error"
		response.Error = "Parameter 'name' erforderlich"
		return response
	}
	if allowedIPs == "" {
		allowedIPs = "0.0.0.0/0"
	}

	// Optional: DNS fuer Client-Config
	dns, _ := msg.Params["dns"].(string)

	// Server-Name (default: ALWAYSON_VPN)
	serverName := "ALWAYSON_VPN"
	if sn, ok := msg.Params["server_name"].(string); ok && sn != "" {
		serverName = sn
	}

	log.Printf("WG Add Peer: name=%s, server=%s, allowed_ips=%s", name, serverName, allowedIPs)

	// 1. config.xml lesen
	configData, err := os.ReadFile("/conf/config.xml")
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("config.xml lesen: %v", err)
		return response
	}

	// 2. Server-Instanz finden (direkt im XML-Text arbeiten statt Unmarshal,
	//    um die gesamte config.xml nicht zu zerstoeren)
	//    Wir parsen nur den WireGuard-Teil, den Rest lassen wir in Ruhe.

	// Server-Instanz per configctl/PHP auslesen und bearbeiten
	// Sicherer Weg: OPNsense PHP-API nutzen
	serverUUID, serverPubKey, serverPort, tunnelSubnet, err := h.findWGServer(string(configData), serverName)
	if err != nil {
		response.Status = "error"
		response.Error = err.Error()
		return response
	}

	// 3. Naechste freie IP im Subnetz bestimmen
	usedIPs := h.getUsedPeerIPs(string(configData), serverUUID, tunnelSubnet)
	nextIP, err := h.nextFreeIP(tunnelSubnet, usedIPs)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Keine freie IP: %v", err)
		return response
	}

	// 4. Keypair generieren
	privKeyOutput, err := h.runCommand("/usr/bin/wg genkey", 5)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("wg genkey fehlgeschlagen: %v", err)
		return response
	}
	clientPrivKey := strings.TrimSpace(privKeyOutput)

	pubKeyOutput, err := h.runCommand(fmt.Sprintf("echo '%s' | /usr/bin/wg pubkey", clientPrivKey), 5)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("wg pubkey fehlgeschlagen: %v", err)
		return response
	}
	clientPubKey := strings.TrimSpace(pubKeyOutput)

	// 5. UUID fuer neuen Peer generieren
	uuidOutput, err := h.runCommand("/bin/uuidgen", 5)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("uuidgen fehlgeschlagen: %v", err)
		return response
	}
	peerUUID := strings.ToLower(strings.TrimSpace(uuidOutput))

	// 6. Peer in config.xml eintragen (direkt per sed — zuverlaessiger als PHP parse_config)
	err = h.addPeerViaSed(peerUUID, name, clientPubKey, nextIP, serverUUID)
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Peer eintragen fehlgeschlagen: %v", err)
		return response
	}

	// 7. WireGuard neu laden
	reloadOutput, reloadErr := h.runCommand("/usr/local/sbin/configctl wireguard configure", 15)
	if reloadErr != nil {
		log.Printf("WireGuard reload Warnung: %v (%s)", reloadErr, reloadOutput)
	}

	// 8. Endpoint ermitteln
	// Prioritaet: 1. Parameter "endpoint" 2. Default-Gateway-Interface IP 3. hostname
	port := serverPort
	if port == "" {
		port = "51820"
	}

	endpoint := ""
	if ep, ok := msg.Params["endpoint"].(string); ok && ep != "" {
		// Wenn nur Host/IP ohne Port angegeben
		if !strings.Contains(ep, ":") {
			endpoint = fmt.Sprintf("%s:%s", ep, port)
		} else {
			endpoint = ep
		}
	} else {
		// WAN-IP ueber Default-Route Interface ermitteln
		routeOutput, _ := h.runCommand("/sbin/route -n get default 2>/dev/null | /usr/bin/grep interface | /usr/bin/awk '{print $2}'", 5)
		wanIface := strings.TrimSpace(routeOutput)
		if wanIface != "" {
			ipOutput, _ := h.runCommand(fmt.Sprintf("/sbin/ifconfig %s 2>/dev/null | /usr/bin/grep 'inet ' | /usr/bin/awk '{print $2}' | /usr/bin/head -1", wanIface), 5)
			wanIP := strings.TrimSpace(ipOutput)
			if wanIP != "" {
				endpoint = fmt.Sprintf("%s:%s", wanIP, port)
			}
		}
		if endpoint == "" {
			hostnameOutput, _ := h.runCommand("/bin/hostname", 5)
			endpoint = fmt.Sprintf("%s:%s", strings.TrimSpace(hostnameOutput), port)
		}
	}

	// 9. Client-Config zusammenbauen
	clientConfig := fmt.Sprintf(`[Interface]
PrivateKey = %s
Address = %s/32
`, clientPrivKey, nextIP)

	if dns != "" {
		clientConfig += fmt.Sprintf("DNS = %s\n", dns)
	}

	clientConfig += fmt.Sprintf(`
[Peer]
PublicKey = %s
AllowedIPs = %s
Endpoint = %s
PersistentKeepalive = 25
`, serverPubKey, allowedIPs, endpoint)

	response.Status = "ok"
	response.Data = map[string]interface{}{
		"peer_uuid":     peerUUID,
		"name":          name,
		"client_ip":     nextIP,
		"public_key":    clientPubKey,
		"server_pubkey": serverPubKey,
		"endpoint":      endpoint,
		"allowed_ips":   allowedIPs,
		"client_config": clientConfig,
		"server_name":   serverName,
		"server_uuid":   serverUUID,
	}

	log.Printf("WG Peer erstellt: %s (%s) -> %s", name, nextIP, serverName)
	return response
}

// handleWGDeletePeer entfernt einen WireGuard-Peer
func (h *Handler) handleWGDeletePeer(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	peerUUID, _ := msg.Params["peer_uuid"].(string)
	if peerUUID == "" {
		response.Status = "error"
		response.Error = "Parameter 'peer_uuid' erforderlich"
		return response
	}

	log.Printf("WG Delete Peer: uuid=%s", peerUUID)

	// Peer per sed entfernen
	if err := h.removePeerViaSed(peerUUID); err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("Peer loeschen fehlgeschlagen: %v", err)
		return response
	}

	// WireGuard reload
	h.runCommand("/usr/local/sbin/configctl wireguard configure", 15)

	response.Status = "ok"
	response.Data = map[string]interface{}{
		"peer_uuid": peerUUID,
		"message":   "Peer entfernt",
	}

	log.Printf("WG Peer geloescht: %s", peerUUID)
	return response
}

// handleWGListPeers listet alle Peers einer WG-Instanz
func (h *Handler) handleWGListPeers(msg Message) Message {
	response := Message{Type: "response", ID: msg.ID}

	serverName := "ALWAYSON_VPN"
	if sn, ok := msg.Params["server_name"].(string); ok && sn != "" {
		serverName = sn
	}

	configData, err := os.ReadFile("/conf/config.xml")
	if err != nil {
		response.Status = "error"
		response.Error = fmt.Sprintf("config.xml lesen: %v", err)
		return response
	}

	configStr := string(configData)
	serverUUID, _, _, tunnelSubnet, err := h.findWGServer(configStr, serverName)
	if err != nil {
		response.Status = "error"
		response.Error = err.Error()
		return response
	}

	// Peer-UUIDs aus Server lesen
	peerUUIDs := h.getServerPeerUUIDs(configStr, serverUUID)

	// Peer-Details sammeln
	var peers []map[string]interface{}
	for _, uuid := range peerUUIDs {
		peer := h.getPeerByUUID(configStr, uuid)
		if peer != nil {
			peers = append(peers, peer)
		}
	}

	if peers == nil {
		peers = []map[string]interface{}{}
	}

	// Live WG-Status (Handshake, Transfer)
	wgStatus, _ := h.runCommand("/usr/bin/wg show all", 5)

	response.Status = "ok"
	response.Data = map[string]interface{}{
		"server_name": serverName,
		"server_uuid": serverUUID,
		"subnet":      tunnelSubnet,
		"peer_count":  len(peers),
		"peers":       peers,
		"wg_status":   strings.TrimSpace(wgStatus),
	}

	return response
}

// --- Helper Functions ---

// findWGServer sucht eine WG-Server-Instanz nach Name in der config.xml
func (h *Handler) findWGServer(configXML, serverName string) (uuid, pubkey, port, tunnelAddr string, err error) {
	type serverEntry struct {
		UUID          string `xml:"uuid,attr"`
		Name          string `xml:"name"`
		PubKey        string `xml:"pubkey"`
		Port          string `xml:"port"`
		TunnelAddress string `xml:"tunneladdress"`
		Peers         string `xml:"peers"`
	}

	// Einfaches XML-Parsing nur fuer den WG-Teil
	type opnsenseWG struct {
		Servers []serverEntry `xml:"OPNsense>wireguard>server>servers>server"`
	}

	var cfg opnsenseWG
	if xmlErr := xml.Unmarshal([]byte(configXML), &cfg); xmlErr != nil {
		return "", "", "", "", fmt.Errorf("XML parsen: %v", xmlErr)
	}

	for _, srv := range cfg.Servers {
		if srv.Name == serverName {
			return srv.UUID, srv.PubKey, srv.Port, srv.TunnelAddress, nil
		}
	}

	return "", "", "", "", fmt.Errorf("WireGuard-Server '%s' nicht gefunden", serverName)
}

// getUsedPeerIPs sammelt alle vergebenen IPs im Server-Subnetz
func (h *Handler) getUsedPeerIPs(configXML, serverUUID, tunnelAddr string) []string {
	type clientEntry struct {
		UUID          string `xml:"uuid,attr"`
		TunnelAddress string `xml:"tunneladdress"`
	}
	type opnsenseClients struct {
		Clients []clientEntry `xml:"OPNsense>wireguard>client>clients>client"`
	}

	var cfg opnsenseClients
	xml.Unmarshal([]byte(configXML), &cfg)

	peerUUIDs := h.getServerPeerUUIDs(configXML, serverUUID)
	peerSet := make(map[string]bool)
	for _, u := range peerUUIDs {
		peerSet[u] = true
	}

	var ips []string
	for _, c := range cfg.Clients {
		if peerSet[c.UUID] {
			// tunneladdress kann "10.33.144.2/32" sein
			addr := strings.Split(c.TunnelAddress, "/")[0]
			if addr != "" {
				ips = append(ips, addr)
			}
		}
	}

	return ips
}

// getServerPeerUUIDs liest die Peer-UUIDs aus dem Server-Eintrag
func (h *Handler) getServerPeerUUIDs(configXML, serverUUID string) []string {
	type serverEntry struct {
		UUID  string `xml:"uuid,attr"`
		Peers string `xml:"peers"`
	}
	type opnsenseServers struct {
		Servers []serverEntry `xml:"OPNsense>wireguard>server>servers>server"`
	}

	var cfg opnsenseServers
	xml.Unmarshal([]byte(configXML), &cfg)

	for _, srv := range cfg.Servers {
		if srv.UUID == serverUUID {
			var uuids []string
			for _, p := range strings.Split(srv.Peers, ",") {
				p = strings.TrimSpace(p)
				if p != "" {
					uuids = append(uuids, p)
				}
			}
			return uuids
		}
	}
	return nil
}

// getPeerByUUID liest Peer-Details aus config.xml
func (h *Handler) getPeerByUUID(configXML, uuid string) map[string]interface{} {
	type clientEntry struct {
		UUID          string `xml:"uuid,attr"`
		Enabled       string `xml:"enabled"`
		Name          string `xml:"name"`
		PubKey        string `xml:"pubkey"`
		TunnelAddress string `xml:"tunneladdress"`
	}
	type opnsenseClients struct {
		Clients []clientEntry `xml:"OPNsense>wireguard>client>clients>client"`
	}

	var cfg opnsenseClients
	xml.Unmarshal([]byte(configXML), &cfg)

	for _, c := range cfg.Clients {
		if c.UUID == uuid {
			return map[string]interface{}{
				"uuid":           c.UUID,
				"enabled":        c.Enabled,
				"name":           c.Name,
				"public_key":     c.PubKey,
				"tunnel_address": c.TunnelAddress,
			}
		}
	}
	return nil
}

// nextFreeIP berechnet die naechste freie IP in einem Subnetz
func (h *Handler) nextFreeIP(tunnelAddr string, usedIPs []string) (string, error) {
	// tunnelAddr: "10.33.144.1/24"
	_, ipNet, err := net.ParseCIDR(tunnelAddr)
	if err != nil {
		return "", fmt.Errorf("Subnetz parsen: %v", err)
	}

	usedSet := make(map[string]bool)
	// Server-IP selbst ist auch belegt
	serverIP := strings.Split(tunnelAddr, "/")[0]
	usedSet[serverIP] = true
	for _, ip := range usedIPs {
		usedSet[ip] = true
	}

	// Ab .2 durchiterieren
	ip := make(net.IP, len(ipNet.IP))
	copy(ip, ipNet.IP)

	for i := 2; i < 255; i++ {
		candidate := make(net.IP, len(ip))
		copy(candidate, ip)
		candidate[len(candidate)-1] = byte(i)

		if !ipNet.Contains(candidate) {
			break
		}
		if !usedSet[candidate.String()] {
			return candidate.String(), nil
		}
	}

	return "", fmt.Errorf("kein freier IP-Platz im Subnetz %s", tunnelAddr)
}

// addPeerViaSed fuegt Peer per sed-Kommandos in config.xml ein
// Zuverlaessiger als PHP parse_config, behandelt <peers/> und <peers>...</peers> korrekt
func (h *Handler) addPeerViaSed(peerUUID, name, pubKey, clientIP, serverUUID string) error {
	configPath := "/conf/config.xml"

	// 1. Client-Block vor </clients> einfuegen (direkt in Go)
	configData, err := os.ReadFile(configPath)
	if err != nil {
		return fmt.Errorf("config.xml lesen: %v", err)
	}

	clientBlock := fmt.Sprintf(
		"\n          <client uuid=\"%s\">\n            <enabled>1</enabled>\n            <name>%s</name>\n            <pubkey>%s</pubkey>\n            <psk/>\n            <tunneladdress>%s/32</tunneladdress>\n            <serveraddress/>\n            <serverport/>\n            <keepalive/>\n          </client>\n        ",
		peerUUID, name, pubKey, clientIP)

	configStr := string(configData)
	// Vor dem letzten </clients> einfuegen
	lastIdx := strings.LastIndex(configStr, "</clients>")
	if lastIdx < 0 {
		return fmt.Errorf("</clients> nicht in config.xml gefunden")
	}
	configStr = configStr[:lastIdx] + clientBlock + configStr[lastIdx:]

	if err := os.WriteFile(configPath, []byte(configStr), 0644); err != nil {
		return fmt.Errorf("config.xml schreiben: %v", err)
	}

	// 2. Peer-UUID zum Server hinzufuegen
	// Config nochmal lesen (wurde gerade geschrieben)
	configData2, err2 := os.ReadFile(configPath)
	if err2 != nil {
		return fmt.Errorf("config.xml lesen: %v", err2)
	}
	configStr = string(configData2)

	serverTag := fmt.Sprintf(`uuid="%s"`, serverUUID)
	idx := strings.Index(configStr, serverTag)
	if idx < 0 {
		return fmt.Errorf("Server %s nicht gefunden", serverUUID)
	}

	// Suche im Server-Block (max 2000 Zeichen nach dem UUID-Tag)
	searchEnd := idx + 2000
	if searchEnd > len(configStr) {
		searchEnd = len(configStr)
	}
	searchFrom := configStr[idx:searchEnd]

	// Fall 1: <peers/> (self-closing, leer)
	selfClosingIdx := strings.Index(searchFrom, "<peers/>")
	if selfClosingIdx >= 0 {
		absIdx := idx + selfClosingIdx
		newPeersTag := fmt.Sprintf("<peers>%s</peers>", peerUUID)
		configStr = configStr[:absIdx] + newPeersTag + configStr[absIdx+8:]
	} else {
		// Fall 2: <peers>...</peers>
		peersStart := strings.Index(searchFrom, "<peers>")
		peersEnd := strings.Index(searchFrom, "</peers>")
		if peersStart >= 0 && peersEnd >= 0 {
			absStart := idx + peersStart + 7
			absEnd := idx + peersEnd
			currentPeers := strings.TrimSpace(configStr[absStart:absEnd])
			var newPeers string
			if currentPeers == "" {
				newPeers = peerUUID
			} else {
				newPeers = currentPeers + "," + peerUUID
			}
			configStr = configStr[:absStart] + newPeers + configStr[absEnd:]
		} else {
			return fmt.Errorf("peers Tag nicht im Server-Block gefunden")
		}
	}

	return os.WriteFile(configPath, []byte(configStr), 0644)
}

// removePeerViaSed entfernt einen Peer per sed aus config.xml
func (h *Handler) removePeerViaSed(peerUUID string) error {
	configPath := "/conf/config.xml"

	// 1. Client-Block entfernen (alles von <client uuid="..."> bis </client>)
	sedRemove := fmt.Sprintf(
		`/usr/bin/sed -i '' '/<client uuid="%s">/,/<\/client>/d' %s`,
		peerUUID, configPath)
	if _, err := h.runCommand(sedRemove, 5); err != nil {
		return fmt.Errorf("Client entfernen: %v", err)
	}

	// 2. UUID aus allen <peers>-Tags entfernen
	configData, err := os.ReadFile(configPath)
	if err != nil {
		return err
	}
	configStr := string(configData)

	// Alle Vorkommen der UUID in <peers>-Tags bereinigen
	// Varianten: "uuid", "uuid,other", "other,uuid", "other,uuid,more"
	replacements := []struct{ old, new string }{
		{fmt.Sprintf("<peers>%s,", peerUUID), "<peers>"},
		{fmt.Sprintf(",%s</peers>", peerUUID), "</peers>"},
		{fmt.Sprintf("<peers>%s</peers>", peerUUID), "<peers/>"},
		{fmt.Sprintf(",%s,", peerUUID), ","},
	}

	for _, r := range replacements {
		configStr = strings.ReplaceAll(configStr, r.old, r.new)
	}

	return os.WriteFile(configPath, []byte(configStr), 0644)
}
