"""Utility functions for port management."""

import random
import socket
from logging import INFO

from flwr.common import log


class PortNotFoundError(Exception):
    """Exception raised for not finding a free port after 100 attempts."""


def get_free_tcp_port(excluded_ports: list[int] | None = None) -> int:
    """Find a free TCP port while excluding specified ports.

    This function finds an available TCP port by attempting to bind to random ports
    in the range 1024-65535, excluding any ports specified in the excluded_ports list.
    It returns the first available port that is not in the exclusion list.

    Parameters
    ----------
    excluded_ports : list[int] | None, optional
        list of ports to exclude from consideration, by default None.

    Raises
    ------
    PortNotFoundError
        If no free ports are found after 100 attempts.

    Returns
    -------
    int
        An available TCP port that is not in the excluded_ports list.

    """
    if excluded_ports is None:
        excluded_ports = []

    # Try up to 100 times to find a free port
    for _ in range(100):
        # Generate a random port between 1024 and 65535
        port = random.randint(1024, 65535)  # noqa: S311

        # Skip if port is in excluded list
        if port in excluded_ports:
            continue

        # Try to bind to the port
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("", port))
            except OSError:
                continue
            else:
                return port

    raise PortNotFoundError


if __name__ == "__main__":
    # When run directly, log a free port
    log(INFO, "port_utils: got the port: %s", get_free_tcp_port())
