package main

import (
	"bufio"
	"context"
	"encoding/csv"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gdamore/tcell/v2"
)

// ===========================================================================
// PORT SCANNER MODULE (Module 7) - Data + Core + TUI Integration
// ===========================================================================
//
// INTEGRATION NOTES — These edits must be made to main.go for this module:
//
// 1. Add to imports:
//      "encoding/csv"
//      "sort"
//
// 2. Add case to executeCommand() (around line 4495):
//      case ModulePortScanner:
//          tui.portHandleInput("")
//
// 3. Add case to handleFilePickerInput() in the switch tui.activeModule
//    block (around line 6386), BEFORE the default case:
//      case ModulePortScanner:
//          tui.portBulkFromFile(ctx, selected)
//
// ===========================================================================

// ---------------------------------------------------------------------------
// PORT SERVICE NAME MAP (97 well-known port -> service mappings)
// ---------------------------------------------------------------------------

var portServiceNames = map[int]string{
	7:     "echo",
	20:    "ftp-data",
	21:    "ftp",
	22:    "ssh",
	23:    "telnet",
	25:    "smtp",
	37:    "time",
	42:    "nameserver",
	43:    "whois",
	49:    "tacacs",
	53:    "dns",
	67:    "dhcp-server",
	68:    "dhcp-client",
	69:    "tftp",
	70:    "gopher",
	79:    "finger",
	80:    "http",
	81:    "http-alt",
	88:    "kerberos",
	102:   "iso-tsap",
	104:   "acr-nema",
	110:   "pop3",
	111:   "rpcbind",
	113:   "ident",
	119:   "nntp",
	123:   "ntp",
	135:   "msrpc",
	137:   "netbios-ns",
	138:   "netbios-dgm",
	139:   "netbios-ssn",
	143:   "imap",
	161:   "snmp",
	162:   "snmptrap",
	179:   "bgp",
	194:   "irc",
	199:   "smux",
	201:   "at-rtmp",
	264:   "bgmp",
	389:   "ldap",
	443:   "https",
	445:   "microsoft-ds",
	464:   "kpasswd5",
	465:   "smtps",
	497:   "retrospect",
	500:   "isakmp",
	512:   "exec",
	513:   "login",
	514:   "syslog",
	515:   "printer",
	520:   "rip",
	523:   "ibm-db2",
	524:   "ncp",
	540:   "uucp",
	543:   "klogin",
	544:   "kshell",
	548:   "afp",
	554:   "rtsp",
	563:   "nntps",
	587:   "submission",
	593:   "http-rpc-epmap",
	623:   "ipmi",
	631:   "ipp",
	636:   "ldaps",
	646:   "ldp",
	873:   "rsync",
	902:   "vmware-auth",
	990:   "ftps",
	993:   "imaps",
	995:   "pop3s",
	1025:  "nfs-or-iis",
	1080:  "socks",
	1099:  "rmiregistry",
	1433:  "mssql",
	1434:  "mssql-m",
	1521:  "oracle",
	1723:  "pptp",
	1812:  "radius",
	1813:  "radius-acct",
	1900:  "upnp",
	2049:  "nfs",
	2082:  "cpanel",
	2083:  "cpanel-ssl",
	2181:  "zookeeper",
	2222:  "ssh-alt",
	2375:  "docker",
	2376:  "docker-ssl",
	3000:  "grafana",
	3128:  "squid-proxy",
	3268:  "globalcatLDAP",
	3306:  "mysql",
	3389:  "rdp",
	3690:  "svn",
	4443:  "https-alt",
	4444:  "krb524",
	5000:  "upnp-http",
	5060:  "sip",
	5222:  "xmpp-client",
	5269:  "xmpp-server",
	5432:  "postgresql",
	5555:  "freeciv",
	5672:  "amqp",
	5900:  "vnc",
	5984:  "couchdb",
	6379:  "redis",
	6443:  "kubernetes-api",
	6667:  "irc",
	8000:  "http-alt",
	8080:  "http-proxy",
	8443:  "https-alt",
	8888:  "http-alt",
	9090:  "web-console",
	9092:  "kafka",
	9200:  "elasticsearch",
	9300:  "elasticsearch",
	9418:  "git",
	9999:  "abyss",
	10000: "webmin",
	11211: "memcached",
	27017: "mongodb",
	27018: "mongodb-shard",
	28017: "mongodb-web",
	50000: "sap",
}

// ---------------------------------------------------------------------------
// NMAP TOP 1000 PORTS
// ---------------------------------------------------------------------------

var portTop1000 = []int{
	1, 3, 4, 6, 7, 9, 13, 17, 19, 20, 21, 22, 23, 24, 25, 26, 30, 32, 33, 37,
	42, 43, 49, 53, 70, 79, 80, 81, 82, 83, 84, 85, 88, 89, 90, 99, 100, 106,
	109, 110, 111, 113, 119, 125, 135, 139, 143, 144, 146, 161, 163, 179, 199,
	211, 212, 222, 254, 255, 256, 259, 264, 280, 301, 306, 311, 340, 366, 389,
	406, 407, 416, 417, 425, 427, 443, 444, 445, 458, 464, 465, 481, 497, 500,
	512, 513, 514, 515, 524, 541, 543, 544, 545, 548, 554, 555, 563, 587, 593,
	616, 617, 625, 631, 636, 646, 648, 666, 667, 668, 683, 687, 691, 700, 705,
	711, 714, 720, 722, 726, 749, 765, 777, 783, 787, 800, 801, 808, 843, 873,
	880, 888, 898, 900, 901, 902, 903, 911, 912, 981, 987, 990, 992, 993, 995,
	999, 1000, 1001, 1002, 1007, 1009, 1010, 1011, 1021, 1022, 1023, 1024,
	1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036,
	1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048,
	1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060,
	1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072,
	1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084,
	1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096,
	1097, 1098, 1099, 1100, 1102, 1104, 1105, 1106, 1107, 1108, 1110, 1111,
	1112, 1113, 1114, 1117, 1119, 1121, 1122, 1131, 1138, 1148, 1152, 1154,
	1163, 1164, 1165, 1166, 1169, 1174, 1175, 1183, 1185, 1186, 1187, 1192,
	1198, 1199, 1201, 1213, 1216, 1217, 1218, 1233, 1234, 1236, 1244, 1247,
	1248, 1259, 1271, 1272, 1277, 1287, 1296, 1300, 1301, 1309, 1310, 1311,
	1322, 1328, 1334, 1352, 1417, 1433, 1434, 1443, 1455, 1461, 1494, 1500,
	1501, 1503, 1521, 1524, 1533, 1556, 1580, 1583, 1594, 1600, 1641, 1658,
	1666, 1687, 1688, 1700, 1717, 1718, 1719, 1720, 1721, 1723, 1755, 1761,
	1782, 1783, 1801, 1805, 1812, 1839, 1840, 1862, 1863, 1864, 1875, 1900,
	1914, 1935, 1947, 1971, 1972, 1974, 1984, 1998, 1999, 2000, 2001, 2002,
	2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2013, 2020, 2021, 2022,
	2030, 2033, 2034, 2035, 2038, 2040, 2041, 2042, 2043, 2045, 2046, 2047,
	2048, 2049, 2065, 2068, 2099, 2100, 2103, 2105, 2106, 2107, 2111, 2119,
	2121, 2126, 2135, 2144, 2160, 2161, 2170, 2179, 2190, 2191, 2196, 2200,
	2222, 2251, 2260, 2288, 2301, 2323, 2366, 2381, 2382, 2383, 2393, 2394,
	2399, 2401, 2492, 2500, 2522, 2525, 2557, 2601, 2602, 2604, 2605, 2607,
	2608, 2638, 2701, 2702, 2710, 2717, 2718, 2725, 2800, 2809, 2811, 2869,
	2875, 2909, 2910, 2920, 2967, 2968, 2998, 3000, 3001, 3003, 3005, 3006,
	3007, 3011, 3013, 3017, 3030, 3031, 3052, 3071, 3077, 3128, 3168, 3211,
	3221, 3260, 3261, 3268, 3269, 3283, 3300, 3301, 3306, 3322, 3323, 3324,
	3325, 3333, 3351, 3367, 3369, 3370, 3371, 3372, 3389, 3390, 3404, 3476,
	3493, 3517, 3527, 3546, 3551, 3580, 3659, 3689, 3690, 3703, 3737, 3766,
	3784, 3800, 3801, 3809, 3814, 3826, 3827, 3828, 3851, 3869, 3871, 3878,
	3880, 3889, 3905, 3914, 3918, 3920, 3945, 3971, 3986, 3995, 3998, 4000,
	4001, 4002, 4003, 4004, 4005, 4006, 4045, 4111, 4125, 4126, 4129, 4224,
	4242, 4279, 4321, 4343, 4443, 4444, 4445, 4446, 4449, 4550, 4567, 4662,
	4848, 4899, 4900, 4998, 5000, 5001, 5002, 5003, 5004, 5009, 5030, 5033,
	5050, 5051, 5054, 5060, 5061, 5080, 5087, 5100, 5101, 5102, 5120, 5190,
	5200, 5214, 5221, 5222, 5225, 5226, 5269, 5280, 5298, 5357, 5405, 5414,
	5431, 5432, 5440, 5500, 5510, 5544, 5550, 5555, 5560, 5566, 5631, 5633,
	5666, 5678, 5679, 5718, 5730, 5800, 5801, 5802, 5810, 5811, 5815, 5822,
	5825, 5850, 5859, 5862, 5877, 5900, 5901, 5902, 5903, 5904, 5906, 5907,
	5910, 5911, 5915, 5922, 5925, 5950, 5952, 5959, 5960, 5961, 5962, 5963,
	5987, 5988, 5989, 5998, 5999, 6000, 6001, 6002, 6003, 6004, 6005, 6006,
	6007, 6009, 6025, 6059, 6100, 6101, 6106, 6112, 6123, 6129, 6156, 6346,
	6389, 6502, 6510, 6543, 6547, 6565, 6566, 6567, 6580, 6646, 6666, 6667,
	6668, 6669, 6689, 6692, 6699, 6779, 6788, 6789, 6792, 6839, 6881, 6901,
	6969, 7000, 7001, 7002, 7004, 7007, 7019, 7025, 7070, 7100, 7103, 7106,
	7200, 7201, 7402, 7435, 7443, 7496, 7512, 7625, 7627, 7676, 7741, 7777,
	7778, 7800, 7911, 7920, 7921, 7937, 7938, 7999, 8000, 8001, 8002, 8007,
	8008, 8009, 8010, 8011, 8021, 8022, 8031, 8042, 8045, 8080, 8081, 8082,
	8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8093, 8099, 8100, 8180,
	8181, 8192, 8193, 8194, 8200, 8222, 8254, 8290, 8291, 8292, 8300, 8333,
	8383, 8400, 8402, 8443, 8500, 8600, 8649, 8651, 8652, 8654, 8701, 8800,
	8873, 8888, 8899, 8994, 9000, 9001, 9002, 9003, 9009, 9010, 9011, 9040,
	9050, 9071, 9080, 9081, 9090, 9091, 9099, 9100, 9101, 9102, 9103, 9110,
	9111, 9200, 9207, 9220, 9290, 9415, 9418, 9485, 9500, 9502, 9503, 9535,
	9575, 9593, 9594, 9595, 9618, 9666, 9876, 9877, 9878, 9898, 9900, 9917,
	9929, 9943, 9944, 9968, 9998, 9999, 10000, 10001, 10002, 10003, 10004,
	10009, 10010, 10012, 10024, 10025, 10082, 10180, 10215, 10243, 10566,
	10616, 10617, 10621, 10626, 10628, 10629, 10778, 11110, 11111, 11967,
	12000, 12174, 12265, 12345, 13456, 13722, 13782, 13783, 14000, 14238,
	14441, 14442, 15000, 15002, 15003, 15004, 15660, 15742, 16000, 16001,
	16012, 16016, 16018, 16080, 16113, 16992, 16993, 17877, 17988, 18040,
	18101, 18988, 19101, 19283, 19315, 19350, 20000, 20005, 20031, 20221,
	20222, 20828, 21571, 22939, 23502, 24444, 24800, 25734, 25735, 26214,
	27000, 27352, 27353, 27355, 27356, 27715, 28201, 30000, 30718, 30951,
	31038, 31337, 32768, 32769, 32770, 32771, 32772, 32773, 32774, 32775,
	32776, 32777, 32778, 32779, 32780, 32781, 32782, 32783, 32784, 32785,
	33354, 33899, 34571, 34572, 34573, 35500, 38292, 40193, 40911, 41511,
	42510, 44176, 44442, 44443, 44501, 45100, 48080, 49152, 49153, 49154,
	49155, 49156, 49157, 49158, 49159, 49160, 49161, 49163, 49165, 49167,
	49175, 49176, 49400, 49999, 50000, 50001, 50002, 50003, 50006, 50300,
	50389, 50500, 50636, 50800, 51103, 51493, 52673, 52822, 52848, 52869,
	54045, 54328, 55055, 55056, 55555, 55600, 56737, 56738, 57294, 57797,
	58080, 60020, 60443, 61532, 61900, 62078, 63331, 64623, 64680, 65000,
	65129, 65389,
}

// ---------------------------------------------------------------------------
// PORT PARSER: "80,443,8080-8090" -> []int
// ---------------------------------------------------------------------------

func portParsePorts(spec string) ([]int, error) {
	spec = strings.TrimSpace(spec)
	if spec == "" {
		return nil, fmt.Errorf("empty port specification")
	}

	seen := make(map[int]bool)
	var ports []int

	parts := strings.Split(spec, ",")
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}

		if strings.Contains(part, "-") {
			rangeParts := strings.SplitN(part, "-", 2)
			if len(rangeParts) != 2 {
				return nil, fmt.Errorf("invalid range: %s", part)
			}
			startStr := strings.TrimSpace(rangeParts[0])
			endStr := strings.TrimSpace(rangeParts[1])

			start, err := strconv.Atoi(startStr)
			if err != nil {
				return nil, fmt.Errorf("invalid port number: %s", startStr)
			}
			end, err := strconv.Atoi(endStr)
			if err != nil {
				return nil, fmt.Errorf("invalid port number: %s", endStr)
			}

			if start < 1 || start > 65535 || end < 1 || end > 65535 {
				return nil, fmt.Errorf("port out of range (1-65535): %s", part)
			}
			if start > end {
				return nil, fmt.Errorf("invalid range (start > end): %s", part)
			}
			if end-start > 65535 {
				return nil, fmt.Errorf("range too large: %s", part)
			}

			for p := start; p <= end; p++ {
				if !seen[p] {
					seen[p] = true
					ports = append(ports, p)
				}
			}
		} else {
			p, err := strconv.Atoi(part)
			if err != nil {
				return nil, fmt.Errorf("invalid port number: %s", part)
			}
			if p < 1 || p > 65535 {
				return nil, fmt.Errorf("port out of range (1-65535): %d", p)
			}
			if !seen[p] {
				seen[p] = true
				ports = append(ports, p)
			}
		}
	}

	if len(ports) == 0 {
		return nil, fmt.Errorf("no valid ports parsed from: %s", spec)
	}

	sort.Ints(ports)
	return ports, nil
}

// ---------------------------------------------------------------------------
// CORE SCANNER: TCP connect scan with optional banner grab
// ---------------------------------------------------------------------------

func portScanPort(ctx context.Context, address string, port int, timeout time.Duration) (state string, banner string, responseMs int64) {
	target := net.JoinHostPort(address, strconv.Itoa(port))

	start := time.Now()

	dialer := net.Dialer{Timeout: timeout}
	conn, err := dialer.DialContext(ctx, "tcp", target)
	responseMs = time.Since(start).Milliseconds()

	if err != nil {
		if ctx.Err() != nil {
			return "filtered", "", responseMs
		}
		errStr := err.Error()
		// Connection refused = definitely closed
		if strings.Contains(errStr, "refused") || strings.Contains(errStr, "reset") {
			return "closed", "", responseMs
		}
		// Timeout or other = filtered
		return "filtered", "", responseMs
	}
	defer conn.Close()

	// Port is open - attempt a 1-second banner grab
	state = "open"
	conn.SetReadDeadline(time.Now().Add(1 * time.Second))
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err == nil && n > 0 {
		raw := buf[:n]
		// Clean banner: replace non-printable chars, trim
		var cleaned []byte
		for _, b := range raw {
			if b >= 32 && b < 127 {
				cleaned = append(cleaned, b)
			} else if b == '\n' || b == '\r' {
				cleaned = append(cleaned, ' ')
			}
		}
		banner = strings.TrimSpace(string(cleaned))
		if len(banner) > 256 {
			banner = banner[:256]
		}
	}

	return state, banner, responseMs
}

// ---------------------------------------------------------------------------
// portServiceLookup: resolves port number to known service name
// ---------------------------------------------------------------------------

func portServiceLookup(port int) string {
	if name, ok := portServiceNames[port]; ok {
		return name
	}
	return ""
}

// ---------------------------------------------------------------------------
// TUI COMMAND HANDLER: routes menu keys for Port Scanner
// ---------------------------------------------------------------------------

func (tui *TUI) startPortCommand(cmdKey string) {
	tui.currentCmd = cmdKey
	tui.collectedInputs = make(map[string]string)

	switch cmdKey {
	case "1": // Scan target (top 1000)
		tui.inputFields = []string{"target"}
		tui.addOutput("[*] PORT SCAN (Top 1000) -- Enter target IP or hostname")
		tui.inputPrompt = "Enter target"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return

	case "2": // Custom port range — first collect target only
		tui.inputFields = []string{"target"}
		tui.addOutput("[*] CUSTOM PORT SCAN -- Enter target IP or hostname")
		tui.addOutput("[*] You will be prompted for port spec next (e.g. 80,443,8080-8090)")
		tui.inputPrompt = "Enter target"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return

	case "2b": // Internal: second step of custom — collect port spec
		tui.inputFields = []string{"ports"}
		tui.addOutput("[*] Now enter port specification")
		tui.addOutput("[*] Format: 80,443,8080-8090 or 1-65535")
		tui.inputPrompt = "Enter ports"
		tui.currentField = 0
		tui.inputBuffer = ""
		tui.inputCursor = 0
		tui.commandState = StateInput
		return

	case "3": // Bulk scan from file
		files, _ := filepath.Glob(filepath.Join(tui.targetsDir, "*.txt"))
		if len(files) > 0 {
			tui.pickerFiles = files
			tui.pickerSelected = 0
			tui.commandState = StateFilePicker
			return
		}
		tui.addOutput("[-] No .txt files found in targets/ folder")
		tui.addOutput("[*] Place target files in: " + tui.targetsDir)
		return

	case "4": // Stats
		tui.showPortStats()
		return

	case "5": // Export CSV
		tui.exportPortResults()
		return

	case "6": // Clear
		tui.clearOutput()
		tui.addOutput("[*] Output cleared")
		return

	case "Q", "q":
		tui.running = false
		return
	}
}

// ---------------------------------------------------------------------------
// portHandleInput: processes collected input after StateInput completes.
// Called from executeCommand() when activeModule == ModulePortScanner.
// The input parameter is unused (inputs live in tui.collectedInputs) but
// kept in the signature for interface consistency.
// ---------------------------------------------------------------------------

func (tui *TUI) portHandleInput(input string) {
	switch tui.currentCmd {
	case "1": // Scan top 1000 — target was collected
		tui.commandState = StateRunning
		tui.Render()
		ctx, cancel := context.WithCancel(context.Background())
		tui.cancelScan = cancel
		go func() {
			defer func() { tui.cancelScan = nil }()
			target := tui.collectedInputs["target"]
			tui.runPortScan(ctx, target, portTop1000, "top1000")
			if ctx.Err() == nil {
				tui.commandState = StateComplete
			}
			tui.Render()
		}()

	case "2": // Custom range step 1 — target was collected, now need ports
		// Preserve the target we already collected, then prompt for ports
		savedTarget := tui.collectedInputs["target"]
		tui.currentCmd = "2b"
		tui.collectedInputs = make(map[string]string)
		tui.collectedInputs["target"] = savedTarget
		tui.startPortCommand("2b")

	case "2b": // Custom range step 2 — ports were collected
		portSpec := tui.collectedInputs["ports"]
		ports, err := portParsePorts(portSpec)
		if err != nil {
			tui.addOutput(fmt.Sprintf("[-] Invalid port spec: %v", err))
			tui.commandState = StateMenu
			tui.Render()
			return
		}
		tui.commandState = StateRunning
		tui.Render()
		ctx, cancel := context.WithCancel(context.Background())
		tui.cancelScan = cancel
		go func() {
			defer func() { tui.cancelScan = nil }()
			target := tui.collectedInputs["target"]
			tui.runPortScan(ctx, target, ports, "custom:"+portSpec)
			if ctx.Err() == nil {
				tui.commandState = StateComplete
			}
			tui.Render()
		}()
	}
}

// ---------------------------------------------------------------------------
// portBulkFromFile: called from handleFilePickerInput when a file is selected
// for the Port Scanner module. Reads targets from file, scans top 1000 on each.
// ---------------------------------------------------------------------------

func (tui *TUI) portBulkFromFile(ctx context.Context, filePath string) {
	file, err := os.Open(filePath)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error opening file: %v", err))
		return
	}

	var targets []string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line != "" && !strings.HasPrefix(line, "#") {
			targets = append(targets, line)
		}
	}
	file.Close()

	if len(targets) == 0 {
		tui.addOutput("[-] No targets found in file")
		return
	}

	tui.runPortBulkScan(ctx, targets, portTop1000, "top1000-bulk")
}

// ---------------------------------------------------------------------------
// runPortScan: worker pool TCP scan with progress, DNS resolution, DB storage
// ---------------------------------------------------------------------------

func (tui *TUI) runPortScan(ctx context.Context, address string, ports []int, scanType string) {
	address = strings.TrimSpace(address)
	address = strings.TrimPrefix(address, "http://")
	address = strings.TrimPrefix(address, "https://")
	address = strings.TrimRight(address, "/")
	// Strip port if accidentally included
	if host, _, err := net.SplitHostPort(address); err == nil {
		address = host
	}

	tui.startLog("port-scan")
	defer tui.closeLog()

	// DNS resolution
	tui.addOutput(fmt.Sprintf("[*] Resolving %s...", address))
	tui.writeLog("TARGET: %s", address)

	ips, err := net.LookupHost(address)
	resolvedIP := address
	if err == nil && len(ips) > 0 {
		resolvedIP = ips[0]
		if resolvedIP != address {
			tui.addOutput(fmt.Sprintf("[+] Resolved: %s -> %s", address, resolvedIP))
			tui.writeLog("RESOLVED: %s -> %s", address, resolvedIP)
		}
	} else {
		tui.addOutput(fmt.Sprintf("[*] Using address directly: %s", address))
	}

	totalPorts := int64(len(ports))
	atomic.StoreInt64(&tui.portTotal, totalPorts)
	atomic.StoreInt64(&tui.portScanned, 0)
	atomic.StoreInt64(&tui.portOpen, 0)
	atomic.StoreInt64(&tui.portClosed, 0)
	atomic.StoreInt64(&tui.portFiltered, 0)
	tui.portCurrentTarget = address

	workers := tui.portWorkers
	if workers < 1 {
		workers = 50
	}
	timeout := tui.portTimeout
	if timeout < time.Millisecond {
		timeout = 3 * time.Second
	}

	tui.addOutput(fmt.Sprintf("[*] Scanning %d ports on %s", totalPorts, address))
	tui.addOutput(fmt.Sprintf("[*] Workers: %d | Timeout: %s", workers, timeout))
	tui.addOutput("[*] Press ESC to cancel")
	tui.addOutput("")
	tui.writeLog("PORTS: %d | WORKERS: %d | TIMEOUT: %s | TYPE: %s", totalPorts, workers, timeout, scanType)

	startTime := time.Now()

	type portResult struct {
		port       int
		state      string
		banner     string
		service    string
		responseMs int64
	}

	workChan := make(chan int, len(ports))
	resultChan := make(chan portResult, workers*2)

	// Feed work
	go func() {
		for _, p := range ports {
			select {
			case <-ctx.Done():
				close(workChan)
				return
			case workChan <- p:
			}
		}
		close(workChan)
	}()

	// Workers
	var wg sync.WaitGroup
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for port := range workChan {
				select {
				case <-ctx.Done():
					return
				default:
				}
				state, banner, ms := portScanPort(ctx, resolvedIP, port, timeout)
				svc := portServiceLookup(port)
				resultChan <- portResult{
					port:       port,
					state:      state,
					banner:     banner,
					service:    svc,
					responseMs: ms,
				}
			}
		}()
	}

	// Close results when workers finish
	go func() {
		wg.Wait()
		close(resultChan)
	}()

	// Collect results
	var openPorts []portResult
	for r := range resultChan {
		scanned := atomic.AddInt64(&tui.portScanned, 1)

		switch r.state {
		case "open":
			atomic.AddInt64(&tui.portOpen, 1)
			openPorts = append(openPorts, r)
			svcLabel := ""
			if r.service != "" {
				svcLabel = " (" + r.service + ")"
			}
			bannerLabel := ""
			if r.banner != "" {
				bannerLabel = " [" + r.banner + "]"
				if len(bannerLabel) > 80 {
					bannerLabel = bannerLabel[:80] + "...]"
				}
			}
			msg := fmt.Sprintf("[+] OPEN  %s:%d%s %dms%s", address, r.port, svcLabel, r.responseMs, bannerLabel)
			tui.addOutput(msg)
			tui.writeLog("OPEN %s:%d %s %dms banner=%q", address, r.port, r.service, r.responseMs, r.banner)
		case "closed":
			atomic.AddInt64(&tui.portClosed, 1)
		case "filtered":
			atomic.AddInt64(&tui.portFiltered, 1)
		}

		// Store each result in DB
		tui.db.conn.Exec(`
			INSERT INTO port_results (target, port, state, service, banner, protocol, response_ms, discovered)
			VALUES (?, ?, ?, ?, ?, 'tcp', ?, CURRENT_TIMESTAMP)
			ON CONFLICT(target, port, protocol) DO UPDATE SET
				state = excluded.state,
				service = excluded.service,
				banner = excluded.banner,
				response_ms = excluded.response_ms,
				discovered = CURRENT_TIMESTAMP`,
			address, r.port, r.state, r.service, r.banner, r.responseMs)

		// Progress update every 50 ports or on open findings
		if scanned%50 == 0 || r.state == "open" {
			tui.Render()
		}
	}

	elapsed := time.Since(startTime)
	openCount := atomic.LoadInt64(&tui.portOpen)
	closedCount := atomic.LoadInt64(&tui.portClosed)
	filteredCount := atomic.LoadInt64(&tui.portFiltered)

	// Update port_targets summary
	tui.db.conn.Exec(`
		INSERT INTO port_targets (target, scan_type, port_spec, open_count, closed_count, filtered_count, first_scanned, last_scanned)
		VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
		ON CONFLICT(target) DO UPDATE SET
			scan_type = excluded.scan_type,
			port_spec = excluded.port_spec,
			open_count = excluded.open_count,
			closed_count = excluded.closed_count,
			filtered_count = excluded.filtered_count,
			last_scanned = CURRENT_TIMESTAMP`,
		address, scanType, scanType, openCount, closedCount, filteredCount)

	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[*] === Scan Complete: %s ===", address))
	tui.addOutput(fmt.Sprintf("[*] Duration: %s", elapsed.Round(time.Millisecond)))
	tui.addOutput(fmt.Sprintf("[*] Open: %d | Closed: %d | Filtered: %d | Total: %d",
		openCount, closedCount, filteredCount, totalPorts))

	if len(openPorts) > 0 {
		tui.addOutput("")
		tui.addOutput("[*] --- Open Ports Summary ---")
		// Sort open ports by port number
		sort.Slice(openPorts, func(i, j int) bool {
			return openPorts[i].port < openPorts[j].port
		})
		for _, op := range openPorts {
			svc := op.service
			if svc == "" {
				svc = "unknown"
			}
			line := fmt.Sprintf("    %d/tcp  %-20s %dms", op.port, svc, op.responseMs)
			if op.banner != "" {
				line += "  " + op.banner
			}
			tui.addOutput(line)
		}
	}
	tui.addOutput("")

	tui.writeLog("COMPLETE: %s elapsed, open=%d closed=%d filtered=%d", elapsed.Round(time.Millisecond), openCount, closedCount, filteredCount)
}

// ---------------------------------------------------------------------------
// runPortBulkScan: iterates targets list, runs port scan on each
// ---------------------------------------------------------------------------

func (tui *TUI) runPortBulkScan(ctx context.Context, targets []string, ports []int, scanType string) {
	tui.startLog("port-bulk")
	defer tui.closeLog()

	total := len(targets)
	tui.addOutput(fmt.Sprintf("[*] BULK PORT SCAN -- %d targets, %d ports each", total, len(ports)))
	tui.addOutput(fmt.Sprintf("[*] Scan type: %s", scanType))
	tui.addOutput("")
	tui.writeLog("BULK SCAN: %d targets, %d ports, type=%s", total, len(ports), scanType)

	for i, target := range targets {
		select {
		case <-ctx.Done():
			tui.addOutput("[!] Bulk scan cancelled")
			tui.writeLog("CANCELLED at target %d/%d", i+1, total)
			return
		default:
		}

		target = strings.TrimSpace(target)
		if target == "" || strings.HasPrefix(target, "#") {
			continue
		}

		tui.addOutput(fmt.Sprintf("[*] === Target %d/%d: %s ===", i+1, total, target))
		tui.writeLog("--- Target %d/%d: %s ---", i+1, total, target)
		tui.runPortScan(ctx, target, ports, scanType)
	}

	tui.addOutput("")
	tui.addOutput(fmt.Sprintf("[*] Bulk scan complete: %d targets processed", total))
	tui.writeLog("BULK COMPLETE: %d targets", total)
}

// ---------------------------------------------------------------------------
// showPortStats: query DB and display statistics
// ---------------------------------------------------------------------------

func (tui *TUI) showPortStats() {
	var totalTargets, totalResults, totalOpen, totalClosed, totalFiltered int

	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_targets").Scan(&totalTargets)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results").Scan(&totalResults)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results WHERE state='open'").Scan(&totalOpen)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results WHERE state='closed'").Scan(&totalClosed)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results WHERE state='filtered'").Scan(&totalFiltered)

	tui.addOutput("")
	tui.addOutput("[*] === Port Scanner Statistics ===")
	tui.addOutput(fmt.Sprintf("    Targets scanned: %d", totalTargets))
	tui.addOutput(fmt.Sprintf("    Total results:   %d", totalResults))
	tui.addOutput(fmt.Sprintf("    Open ports:      %d", totalOpen))
	tui.addOutput(fmt.Sprintf("    Closed ports:    %d", totalClosed))
	tui.addOutput(fmt.Sprintf("    Filtered ports:  %d", totalFiltered))

	// Top 10 most common open ports
	rows, err := tui.db.conn.Query(`
		SELECT port, service, COUNT(*) as cnt
		FROM port_results WHERE state='open'
		GROUP BY port ORDER BY cnt DESC LIMIT 10`)
	if err == nil {
		defer rows.Close()
		tui.addOutput("")
		tui.addOutput("[*] --- Top 10 Open Ports (across all targets) ---")
		for rows.Next() {
			var port, cnt int
			var service string
			rows.Scan(&port, &service, &cnt)
			if service == "" {
				service = "unknown"
			}
			tui.addOutput(fmt.Sprintf("    %d/tcp  %-20s  found on %d target(s)", port, service, cnt))
		}
	}

	// Recent scans
	rows2, err := tui.db.conn.Query(`
		SELECT target, scan_type, open_count, closed_count, filtered_count, last_scanned
		FROM port_targets ORDER BY last_scanned DESC LIMIT 10`)
	if err == nil {
		defer rows2.Close()
		tui.addOutput("")
		tui.addOutput("[*] --- Recent Scans ---")
		for rows2.Next() {
			var target, scanType, lastScanned string
			var openC, closedC, filteredC int
			rows2.Scan(&target, &scanType, &openC, &closedC, &filteredC, &lastScanned)
			tui.addOutput(fmt.Sprintf("    %-30s  open=%d closed=%d filtered=%d  [%s] %s",
				target, openC, closedC, filteredC, scanType, lastScanned))
		}
	}

	tui.addOutput("")
}

// ---------------------------------------------------------------------------
// exportPortResults: export all port scan results to CSV
// ---------------------------------------------------------------------------

func (tui *TUI) exportPortResults() {
	var count int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results").Scan(&count)
	if count == 0 {
		tui.addOutput("[-] No port results to export")
		return
	}

	filename := filepath.Join(tui.logsDir, fmt.Sprintf("port_results_%s.csv", time.Now().Format("2006-01-02_150405")))
	file, err := os.Create(filename)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error creating export: %v", err))
		return
	}
	defer file.Close()

	writer := csv.NewWriter(file)
	defer writer.Flush()

	writer.Write([]string{"target", "port", "state", "service", "banner", "protocol", "response_ms", "discovered"})

	rows, err := tui.db.conn.Query(`
		SELECT target, port, state, service, banner, protocol, response_ms, discovered
		FROM port_results ORDER BY target, port`)
	if err != nil {
		tui.addOutput(fmt.Sprintf("[-] Error querying: %v", err))
		return
	}
	defer rows.Close()

	exported := 0
	for rows.Next() {
		var target, state, service, banner, protocol, discovered string
		var port, responseMs int
		rows.Scan(&target, &port, &state, &service, &banner, &protocol, &responseMs, &discovered)
		writer.Write([]string{
			target,
			strconv.Itoa(port),
			state,
			service,
			banner,
			protocol,
			strconv.Itoa(responseMs),
			discovered,
		})
		exported++
	}

	tui.addOutput(fmt.Sprintf("[+] Exported %d port results to %s", exported, filepath.Base(filename)))
}

// ---------------------------------------------------------------------------
// DASHBOARD RENDERER: Port Scanner module TUI
// ---------------------------------------------------------------------------

func (tui *TUI) renderPortScannerDashboard() {
	modColor := moduleColors[ModulePortScanner]
	borderStyle := tcell.StyleDefault.Foreground(modColor)
	titleStyle := tcell.StyleDefault.Foreground(ColorPrimary).Bold(true)
	textStyle := tcell.StyleDefault.Foreground(ColorText)
	highlightStyle := tcell.StyleDefault.Foreground(modColor)
	successStyle := tcell.StyleDefault.Foreground(ColorSuccess)
	dimStyle := tcell.StyleDefault.Foreground(ColorDim)

	menuWidth := 52
	outputX := menuWidth + 1
	outputWidth := tui.width - menuWidth - 2
	inputHeight := 3
	outputHeight := tui.height - inputHeight - 2

	// Header
	header := fmt.Sprintf(" MOTHERFUCKIN TERMINATOR BOT 9000 v%s | MODULE: Port Scanner ", Version)
	tui.drawString(1, 0, header, titleStyle)

	// Live stats in header
	scanned := atomic.LoadInt64(&tui.portScanned)
	total := atomic.LoadInt64(&tui.portTotal)
	openC := atomic.LoadInt64(&tui.portOpen)
	closedC := atomic.LoadInt64(&tui.portClosed)
	filteredC := atomic.LoadInt64(&tui.portFiltered)

	var pct float64
	if total > 0 {
		pct = float64(scanned) / float64(total) * 100
	}

	statsStr := fmt.Sprintf("Scanned: %d/%d (%.0f%%) | Open: %d | Closed: %d | Filtered: %d ",
		scanned, total, pct, openC, closedC, filteredC)
	tui.drawString(tui.width-len(statsStr)-1, 0, statsStr, dimStyle)

	// Menu box
	tui.drawBox(0, 1, menuWidth, tui.height-inputHeight-1, "COMMANDS", borderStyle)

	menuY := 3
	items := tui.menuItems
	for i, item := range items {
		y := menuY + i
		if y >= tui.height-inputHeight-2 {
			break
		}

		keyStyle := highlightStyle
		nameStyle := textStyle
		if i == tui.selectedItem && !tui.focusOutput {
			tui.fillRect(1, y, menuWidth-2, 1, ' ', tcell.StyleDefault.Background(ColorBorder))
			keyStyle = successStyle.Bold(true).Background(ColorBorder)
			nameStyle = tcell.StyleDefault.Foreground(ColorText).Bold(true).Background(ColorBorder)
		}

		tui.drawString(2, y, fmt.Sprintf("[%s]", item.Key), keyStyle)
		tui.drawStringClipped(6, y, menuWidth-8, item.Name+" - "+item.Desc, nameStyle)
	}

	// Scanner info panel
	infoY := menuY + len(items) + 1
	if infoY < tui.height-inputHeight-2 {
		tui.drawString(2, infoY, "--- Scanner Info ---", dimStyle)

		infoLines := []string{
			fmt.Sprintf(" Top 1000: nmap default ports"),
			fmt.Sprintf(" Services: %d known mappings", len(portServiceNames)),
			fmt.Sprintf(" Workers:  %d concurrent", tui.portWorkers),
			fmt.Sprintf(" Timeout:  %s", tui.portTimeout),
		}

		if tui.portCurrentTarget != "" {
			infoLines = append(infoLines, fmt.Sprintf(" Target:   %s", tui.portCurrentTarget))
		}

		for i, line := range infoLines {
			if infoY+1+i < tui.height-inputHeight-2 {
				tui.drawString(2, infoY+1+i, line, tcell.StyleDefault.Foreground(ColorInfo))
			}
		}

		shortcutY := infoY + len(infoLines) + 2
		if shortcutY < tui.height-inputHeight-2 {
			tui.drawString(2, shortcutY, "--- Shortcuts ---", dimStyle)
			if shortcutY+1 < tui.height-inputHeight-2 {
				tui.drawString(2, shortcutY+1, "F1: Settings  Tab: Module Switch", tcell.StyleDefault.Foreground(ColorInfo))
			}
			if shortcutY+2 < tui.height-inputHeight-2 {
				tui.drawString(2, shortcutY+2, "ESC: Cancel / Back", tcell.StyleDefault.Foreground(ColorInfo))
			}
			if shortcutY+3 < tui.height-inputHeight-2 {
				tui.drawString(2, shortcutY+3, "PgUp/PgDn: Scroll output", tcell.StyleDefault.Foreground(ColorInfo))
			}
		}
	}

	// Output box
	outputTitle := "OUTPUT"
	outputBorder := borderStyle
	if tui.focusOutput {
		outputTitle = "OUTPUT [FOCUSED]"
		outputBorder = tcell.StyleDefault.Foreground(ColorAccent)
	}
	tui.drawBox(outputX, 1, outputWidth, outputHeight, outputTitle, outputBorder)

	tui.outputMutex.Lock()
	maxLines := outputHeight - 2

	if len(tui.outputLines) == 0 {
		tui.drawStringClipped(outputX+2, 3, outputWidth-4,
			"[*] Port Scanner ready -- Select a command to begin", tcell.StyleDefault.Foreground(ColorInfo))
	} else {
		startLine := tui.outputScroll
		for i := 0; i < maxLines && startLine+i < len(tui.outputLines); i++ {
			line := tui.outputLines[startLine+i]
			y := 2 + i

			lineStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite)
			if strings.HasPrefix(line, "[+]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSuccess)
			} else if strings.HasPrefix(line, "[-]") || strings.HasPrefix(line, "[NOT]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorDanger)
			} else if strings.HasPrefix(line, "[ERR]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorWarning)
			} else if strings.HasPrefix(line, "[*]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorInfo)
			} else if strings.HasPrefix(line, "[!]") {
				lineStyle = tcell.StyleDefault.Foreground(ColorSecondary)
			}

			tui.drawStringClipped(outputX+1, y, outputWidth-2, line, lineStyle)
		}

		if len(tui.outputLines) > maxLines {
			scrollInfo := fmt.Sprintf(" %d/%d ", tui.outputScroll+maxLines, len(tui.outputLines))
			tui.drawString(outputX+outputWidth-len(scrollInfo)-1, 1, scrollInfo, dimStyle)
		}
	}
	tui.outputMutex.Unlock()

	// Input box
	inputY := tui.height - inputHeight - 1
	tui.drawBox(0, inputY, tui.width, inputHeight+1, "INPUT", borderStyle)

	if tui.commandState == StateInput {
		promptStyle := tcell.StyleDefault.Foreground(ColorWarning)
		inputStyle := tcell.StyleDefault.Foreground(ColorText)
		prompt := tui.inputPrompt + ": "
		tui.drawString(2, inputY+1, prompt, promptStyle)
		tui.drawString(2+len(prompt), inputY+1, tui.inputBuffer, inputStyle)
		cursorX := 2 + len(prompt) + tui.inputCursor
		if cursorX < tui.width-2 {
			tui.screen.SetContent(cursorX, inputY+1, '\u258C', nil, tcell.StyleDefault.Foreground(ColorPrimary))
		}
	} else if tui.commandState == StateRunning {
		// Show progress bar in input area
		if total > 0 {
			progressStr := fmt.Sprintf("Scanning %s... %d/%d ports (%.0f%%) | Open: %d | ESC to cancel",
				tui.portCurrentTarget, scanned, total, pct, openC)
			tui.drawString(2, inputY+1, progressStr, tcell.StyleDefault.Foreground(ColorWarning))
		} else {
			tui.drawString(2, inputY+1, "Scanning... press ESC to cancel", tcell.StyleDefault.Foreground(ColorWarning))
		}
	} else if tui.commandState == StateComplete {
		tui.drawString(2, inputY+1, "Scan complete! Press any key to continue", successStyle)
	} else {
		hint := "Tab: Module | Arrows: Navigate | Enter: Select | Q: Quit"
		tui.drawStringClipped(2, inputY+1, tui.width-4, hint, dimStyle)
	}

	// Status bar
	statusY := tui.height - 1
	statusStyle := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(modColor)
	tui.fillRect(0, statusY, tui.width, 1, ' ', statusStyle)

	var dbTargets, dbOpen int
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_targets").Scan(&dbTargets)
	tui.db.conn.QueryRow("SELECT COUNT(*) FROM port_results WHERE state='open'").Scan(&dbOpen)

	statusText := fmt.Sprintf(" PORT SCANNER | DB: %d targets | %d open ports | Services: %d | Workers: %d ",
		dbTargets, dbOpen, len(portServiceNames), tui.portWorkers)
	tui.drawString(0, statusY, statusText, statusStyle)
}
