import asyncio
import random
import secrets
import socket
import psutil

# Don't assign rpc or p2p ports lower than this
PORT_MIN = 1024
PORT_MAX = 65535

mapped_ports = {}


async def get_port(n, service):
    key = f"{service}:{n}"
    mapped = mapped_ports.get(key)
    if mapped is not None:
        return mapped

    port = await get_random_unused_port()
    print(f"providing port {port} to {service}:{n}")
    mapped_ports[key] = port
    return port


async def try_bind_port(port: int) -> bool:
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(("localhost", port))
            return True
    except OSError:
        return False


async def is_port_in_use(port: int) -> bool:
    async def check_port():
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            return s.connect_ex(("localhost", port)) == 0

    if await check_port():
        # Random sleep and check again to mitigate race conditions
        await asyncio.sleep(random.uniform(0, 3))
        return await check_port()

    return False


taken_ports = []


async def get_random_unused_port():
    # pylint: disable=global-statement
    global taken_ports
    if len(taken_ports) == 0:
        # first time function is called

        # seed randomness
        seed = int.from_bytes(secrets.token_bytes(16), "big")
        random.seed(seed)

        # try to prefill taken_ports
        try:
            connections = psutil.net_connections()
            taken_ports = {
                conn.laddr.port for conn in connections if conn.status == "LISTEN"
            }
        except psutil.AccessDenied:
            # don't have privileges
            taken_ports = set()
        except Exception as e:
            print(f"Unexpected exception: {e}")
            taken_ports = set()

    while True:
        port = random.randint(PORT_MIN, PORT_MAX)
        if port in taken_ports:
            continue
        taken_ports.add(port)
        if await try_bind_port(port):
            return port


async def p2p_port(n):
    return await get_port(n, "p2p")


async def rpc_port(n):
    return await get_port(n, "rpc")


async def electrum_rpc_port(n):
    return await get_port(n, "electrum_rpc")


async def electrum_ws_port(n):
    return await get_port(n, "electrum_ws")


async def electrum_http_port(n):
    return await get_port(n, "electrum_http")


async def electrum_monitoring_port(n):
    return await get_port(n, "electrum_monitoring")


async def electrum_rpc_ssl_port(n):
    return await get_port(n, "electrum_rpc_ssl")


async def electrum_ws_ssl_port(n):
    return await get_port(n, "electrum_ws_ssl")


async def electrum_https_port(n):
    return await get_port(n, "electrum_https")
