import json
import random
import socket
import struct
import threading
import time
from typing import Any, Dict, Tuple

HOST = "127.0.0.1"
PORT = 8888
CLIENT_DEVICE_ID = 0
MIN_TEST_VLC = 5.0
MAX_TEST_VLC = 6.0
SEND_INTERVAL_SECONDS = 1.0
RECV_TIMEOUT_SECONDS = 1.0
CONNECT_TIMEOUT_SECONDS = 5.0
MAX_PAYLOAD_BYTES = 64 * 1024
HEADER_STRUCT = struct.Struct("<II")


def encode_packet(payload: Dict[str, Any], device_id: int) -> bytes:
    """Encode a JSON payload using the protocol binary header."""
    payload_bytes = json.dumps(
        payload,
        ensure_ascii=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return HEADER_STRUCT.pack(device_id, len(payload_bytes)) + payload_bytes


def recv_exactly(sock: socket.socket, size: int) -> bytes:
    """Receive an exact number of bytes from the socket."""
    chunks = bytearray()

    while len(chunks) < size:
        chunk = sock.recv(size - len(chunks))
        if not chunk:
            raise ConnectionError("Connection closed by peer.")
        chunks.extend(chunk)

    return bytes(chunks)


def read_packet(sock: socket.socket) -> Tuple[int, Any]:
    """Read and decode a single packet from the server."""
    header_bytes = recv_exactly(sock, HEADER_STRUCT.size)
    device_id, payload_length = HEADER_STRUCT.unpack(header_bytes)

    if payload_length > MAX_PAYLOAD_BYTES:
        raise ValueError(
            "Payload length %d exceeds the %d byte limit."
            % (payload_length, MAX_PAYLOAD_BYTES)
        )

    payload_bytes = recv_exactly(sock, payload_length)
    payload = json.loads(payload_bytes.decode("utf-8"))
    return device_id, payload

def send_vlc_message(sock: socket.socket, device_id: int, vlc: float) -> None:
    """Send one test client payload to the server."""
    payload = {"vlc": vlc}
    sock.sendall(encode_packet(payload, device_id=device_id))
    print(f'>>> sent {json.dumps(payload, ensure_ascii=False)} device_id={device_id}')


def receive_loop(sock: socket.socket, stop_event: threading.Event) -> None:
    """Print all packets received from the locator server."""
    while not stop_event.is_set():
        try:
            device_id, payload = read_packet(sock)
        except socket.timeout:
            continue
        except (ConnectionError, OSError, ValueError, json.JSONDecodeError) as error:
            if not stop_event.is_set():
                print("Receiver stopped:", error)
            stop_event.set()
            return

        print(f"<<< received device_id={device_id}")
        print(json.dumps(payload, ensure_ascii=False, indent=2))


def send_loop(sock: socket.socket, stop_event: threading.Event) -> None:
    """Send test vlc packets until the client is stopped."""
    send_vlc_message(
        sock,
        CLIENT_DEVICE_ID,
        round(random.uniform(MIN_TEST_VLC, MAX_TEST_VLC), 2),
    )

    if SEND_INTERVAL_SECONDS <= 0:
        while not stop_event.is_set():
            time.sleep(0.1)
        return

    while not stop_event.is_set():
        time.sleep(SEND_INTERVAL_SECONDS)
        send_vlc_message(
            sock,
            CLIENT_DEVICE_ID,
            round(random.uniform(MIN_TEST_VLC, MAX_TEST_VLC), 2),
        )


def main() -> None:
    """Run the client until interrupted or disconnected."""
    stop_event = threading.Event()

    with socket.create_connection(
        (HOST, PORT),
        timeout=CONNECT_TIMEOUT_SECONDS,
    ) as sock:
        sock.settimeout(RECV_TIMEOUT_SECONDS)
        print(f"Connected to {HOST}:{PORT}")

        receiver = threading.Thread(
            target=receive_loop,
            args=(sock, stop_event),
            daemon=True,
        )
        receiver.start()

        try:
            send_loop(sock, stop_event)
        except KeyboardInterrupt:
            print("Stopping client.")
        finally:
            stop_event.set()


if __name__ == "__main__":
    main()
