import random
import socket
from typing import Union, List

DYNAMIC_SERVICE_PORT_RANGE = (14020, 18020)


def is_host_listening(
    address: str, port: int, timeout: float = 2.0, family=socket.AF_INET, typ=socket.SOCK_STREAM
) -> bool:
    s = socket.socket(family, typ)
    s.settimeout(timeout)
    try:
        err = s.connect_ex((address, port))
        return err == 0
    except socket.error:
        return False
    finally:
        s.close()


def get_free_ports_in_range(
    address: str = "127.0.0.1",
    min_port: int = 49152,
    max_port: int = 65536,
    timeout: float = 0.1,
    family=socket.AF_INET,
    typ=socket.SOCK_STREAM,
    return_after_n_found: Union[None, int] = None,
) -> List[int]:
    free_ports = []
    ports = list(range(min_port, max_port))
    random.shuffle(ports)
    for port in ports:
        if not is_host_listening(address, port, timeout, family, typ):
            free_ports.append(port)
        if return_after_n_found is not None and len(free_ports) >= return_after_n_found:
            break
    return free_ports


def get_free_ports_for_dynamic_service(number) -> List[int]:
    free_ports = get_free_ports_in_range(
        address="127.0.0.1",
        min_port=DYNAMIC_SERVICE_PORT_RANGE[0],
        max_port=DYNAMIC_SERVICE_PORT_RANGE[1],
        timeout=0.01,
        return_after_n_found=number,
    )
    if len(free_ports) != number:
        raise Exception("Failed to find free port(s) for dynamic service")
    return free_ports


def get_free_port_for_dynamic_service() -> int:
    return get_free_ports_for_dynamic_service(1)[0]
