import socket
import select  # provides poll()
import sys
 
PORT        = 9034
MAX_CLIENTS = 100
BUF_SIZE    = 1024
 
 
def make_server_socket() -> socket.socket:
    """Create a TCP socket, bind it to PORT, and start listening."""
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(("", PORT))
    server.listen(10)
    server.setblocking(False)
    print(f"Server listening on port {PORT}")
    return server
 
 
def broadcast(clients: dict, sender_fd: int, data: bytes) -> None:
    """Send data to every connected client except the sender."""
    for fd, sock in list(clients.items()):
        if fd == sender_fd:
            continue
        try:
            sock.sendall(data)
        except OSError as e:
            print(f"send error to fd {fd}: {e}")
 
 
def run_server() -> None:
    server = make_server_socket()
    server_fd = server.fileno()
 
    # poll object tracks which fds we care about
    poller = select.poll()
    poller.register(server_fd, select.POLLIN)
 
    # Map fd -> socket for every active connection (clients only)
    clients: dict[int, socket.socket] = {}
 
    try:
        while True:
            # Block until at least one fd is ready (-1 = no timeout)
            events = poller.poll(-1)
 
            for fd, event in events:
 
                # ── New incoming connection ───────────────────────────
                if fd == server_fd:
                    if len(clients) >= MAX_CLIENTS:
                        print("Too many clients – connection refused")
                        # Accept then immediately close to unblock the peer
                        conn, _ = server.accept()
                        conn.close()
                        continue
 
                    conn, addr = server.accept()
                    conn.setblocking(False)
                    clients[conn.fileno()] = conn
                    poller.register(conn.fileno(), select.POLLIN)
                    print(f"New client connected: {addr} (fd {conn.fileno()})")
 
                # ── Data (or disconnect) from an existing client ───────
                elif event & (select.POLLIN | select.POLLHUP | select.POLLERR):
                    sock = clients.get(fd)
                    if sock is None:
                        continue
 
                    try:
                        data = sock.recv(BUF_SIZE)
                    except OSError as e:
                        print(f"recv error on fd {fd}: {e}")
                        data = b""
 
                    if not data:
                        # Clean disconnect or error – remove the client
                        print(f"Client fd {fd} disconnected")
                        poller.unregister(fd)
                        sock.close()
                        del clients[fd]
                    else:
                        broadcast(clients, fd, data)
 
    except KeyboardInterrupt:
        print("\nShutting down.")
    finally:
        for sock in clients.values():
            sock.close()
        server.close()
 
 
if __name__ == "__main__":
    run_server()
