"""Port and process data collection via psutil."""

from __future__ import annotations

import socket

import psutil

from portui.models import PortInfo

# Maximum display length for command line strings.
CMDLINE_MAX_LEN = 60


class PortDataCollector:
    """Collects port and process information using psutil."""

    @staticmethod
    def get_listening_ports() -> list[PortInfo]:
        """Get all listening ports with process information."""
        ports: list[PortInfo] = []
        seen: set[tuple[str, int, int, str]] = set()  # (ip, port, pid, protocol)

        for proc in psutil.process_iter(
            ["pid", "name", "username", "cmdline", "ppid", "create_time"]
        ):
            try:
                pinfo = proc.info
                if not pinfo["pid"]:
                    continue

                # net_connections() replaces the deprecated connections() in psutil 6+
                connections = proc.net_connections(kind="inet")
                for conn in connections:
                    local_addr = conn.laddr
                    if not local_addr:
                        continue

                    # TCP listening sockets have CONN_LISTEN status.
                    # UDP sockets have None status but are "listening" if bound.
                    is_tcp_listen = (
                        conn.type == socket.SOCK_STREAM
                        and conn.status == psutil.CONN_LISTEN
                    )
                    is_udp_bound = conn.type == socket.SOCK_DGRAM

                    if not (is_tcp_listen or is_udp_bound):
                        continue

                    ip = local_addr.ip
                    port = local_addr.port
                    protocol = "TCP" if conn.type == socket.SOCK_STREAM else "UDP"
                    key = (ip, port, pinfo["pid"], protocol)

                    if key in seen:
                        continue
                    seen.add(key)

                    cmdline = " ".join(pinfo["cmdline"] or [pinfo["name"] or ""])
                    if len(cmdline) > CMDLINE_MAX_LEN:
                        cmdline = cmdline[: CMDLINE_MAX_LEN - 3] + "..."

                    port_info = PortInfo(
                        port=port,
                        ip=ip,
                        protocol=protocol,
                        pid=pinfo["pid"],
                        process_name=pinfo["name"] or "[unknown]",
                        username=pinfo["username"] or "[unknown]",
                        state=(
                            conn.status or "LISTEN"
                            if protocol == "UDP"
                            else conn.status
                        ),
                        cmdline=cmdline,
                        ppid=pinfo.get("ppid") or 0,
                        create_time=pinfo.get("create_time") or 0.0,
                    )
                    ports.append(port_info)

            except (psutil.NoSuchProcess, psutil.AccessDenied):
                continue

        return sorted(ports, key=lambda p: (p.port, p.ip))

    @staticmethod
    def kill_process(
        pid: int, force: bool = False, expected_create_time: float | None = None
    ) -> tuple[bool, str]:
        """Kill a process. Returns (success, message).

        If *expected_create_time* is provided the process create_time is
        verified before sending any signal.  A mismatch means the PID was
        recycled and the operation is aborted.
        """
        try:
            proc = psutil.Process(pid)

            # PID-recycling guard: verify the process is the one we selected.
            if expected_create_time is not None:
                actual = proc.create_time()
                if abs(actual - expected_create_time) > 0.01:
                    return (
                        False,
                        f"PID {pid} has been recycled — refusing to kill",
                    )

            name = proc.name()
            if force:
                proc.kill()
                return True, f"Force killed {name} (PID {pid})"
            else:
                proc.terminate()
                return True, f"Terminated {name} (PID {pid})"
        except psutil.NoSuchProcess:
            return False, f"Process {pid} no longer exists"
        except psutil.AccessDenied:
            return False, f"Permission denied to kill process {pid}"
        except (OSError, psutil.Error) as e:
            return False, f"Error killing process {pid}: {e}"
