//go:build !windows

package client

import (
	"os/exec"
	"strconv"
	"strings"
)

// collectListenPorts parses ss -tlnp output for all TCP listeners.
// Falls back to netstat -tlnp if ss is unavailable.
func collectListenPorts() *ListenPortsReport {
	r := &ListenPortsReport{}

	// Prefer ss (iproute2) — faster and widely available on modern Linux
	if out, err := exec.Command("ss", "-tlnp").Output(); err == nil {
		parseSSOutput(r, string(out))
		if len(r.Ports) > 0 {
			r.Count = len(r.Ports)
			return r
		}
	}

	// Fallback: netstat -tlnp (net-tools, older systems)
	if out, err := exec.Command("netstat", "-tlnp").Output(); err == nil {
		parseNetstatOutput(r, string(out))
	}

	r.Count = len(r.Ports)
	return r
}

// parseSSOutput parses `ss -tlnp` lines.
// Example line:
// LISTEN  0  128  0.0.0.0:22   0.0.0.0:*  users:(("sshd",pid=1234,fd=3))
func parseSSOutput(r *ListenPortsReport, raw string) {
	seen := map[int]bool{}
	for _, line := range strings.Split(raw, "\n") {
		fields := strings.Fields(line)
		if len(fields) < 5 || fields[0] != "LISTEN" {
			continue
		}
		lp := ListenPort{Proto: "tcp"}
		// Local address is field 4 (index 3)
		localAddr := fields[3]
		host, portStr, ok := splitHostPort(localAddr)
		if !ok {
			continue
		}
		port, err := strconv.Atoi(portStr)
		if err != nil || port <= 0 || seen[port] {
			continue
		}
		seen[port] = true
		lp.Port = port
		lp.Addr = host

		// Parse process info from users:(("name",pid=N,...))
		for _, f := range fields[4:] {
			if strings.HasPrefix(f, "users:(") {
				// users:(("sshd",pid=1234,fd=3))
				inner := strings.TrimPrefix(f, "users:((")
				inner = strings.TrimSuffix(inner, "))")
				parts := strings.Split(inner, ",")
				if len(parts) >= 1 {
					lp.Process = strings.Trim(parts[0], `"`)
				}
				for _, p := range parts {
					if strings.HasPrefix(p, "pid=") {
						if n, e := strconv.Atoi(strings.TrimPrefix(p, "pid=")); e == nil {
							lp.PID = n
						}
					}
				}
			}
		}
		r.Ports = append(r.Ports, lp)
	}
}

// parseNetstatOutput parses `netstat -tlnp` lines.
// Example: tcp  0  0  0.0.0.0:22  0.0.0.0:*  LISTEN  1234/sshd
func parseNetstatOutput(r *ListenPortsReport, raw string) {
	seen := map[int]bool{}
	for _, line := range strings.Split(raw, "\n") {
		fields := strings.Fields(line)
		if len(fields) < 6 {
			continue
		}
		proto := strings.ToLower(fields[0])
		if !strings.HasPrefix(proto, "tcp") {
			continue
		}
		state := fields[5]
		if strings.ToUpper(state) != "LISTEN" {
			continue
		}
		lp := ListenPort{Proto: "tcp"}
		host, portStr, ok := splitHostPort(fields[3])
		if !ok {
			continue
		}
		port, err := strconv.Atoi(portStr)
		if err != nil || port <= 0 || seen[port] {
			continue
		}
		seen[port] = true
		lp.Port = port
		lp.Addr = host

		// PID/program: field 6 if present, e.g. "1234/sshd"
		if len(fields) > 6 {
			pidProg := fields[6]
			parts := strings.SplitN(pidProg, "/", 2)
			if n, e := strconv.Atoi(parts[0]); e == nil {
				lp.PID = n
			}
			if len(parts) == 2 {
				lp.Process = parts[1]
			}
		}
		r.Ports = append(r.Ports, lp)
	}
}

// splitHostPort handles both IPv4 (host:port) and IPv6 ([::]:port) addresses.
func splitHostPort(addr string) (host, port string, ok bool) {
	if strings.HasPrefix(addr, "[") {
		// IPv6: [::1]:22
		end := strings.LastIndex(addr, "]")
		if end < 0 {
			return
		}
		host = addr[1:end]
		rest := addr[end+1:]
		if !strings.HasPrefix(rest, ":") {
			return
		}
		port = rest[1:]
		ok = true
		return
	}
	// IPv4: 0.0.0.0:22
	idx := strings.LastIndex(addr, ":")
	if idx < 0 {
		return
	}
	host = addr[:idx]
	port = addr[idx+1:]
	ok = true
	return
}
