"""Radar sensor emulator compatible with Synapse radar controller."""

from __future__ import annotations

import logging
import math
import socket
import struct
import threading
import time
from dataclasses import dataclass
from typing import Optional

logger = logging.getLogger(__name__)


@dataclass(slots=True)
class RadarEmulatorConfig:
    host: str = "127.0.0.1"
    ctrl_port: int = 8100
    data_port: int = 8101
    n_samples: int = 511
    header_fields: int = 16
    sample_bytes: int = 4
    frame_interval_s: float = 0.02
    status_flags: int = 0x00000001
    adc1_level: int = 82
    adc2_level: int = 79
    cpu_temp_c: float = 48.0
    board_temp_c: float = 42.0


class RadarEmulator:
    """Implements the minimal command/data protocol Synapse expects."""

    def __init__(self, config: RadarEmulatorConfig):
        self.config = config
        self._stop_event = threading.Event()
        self._state_lock = threading.Lock()
        self._is_running = False
        self._irf_enabled = True
        self._measurement_limit = 0
        self._frame_counter = 0

        self._ctrl_server: Optional[socket.socket] = None
        self._data_server: Optional[socket.socket] = None
        self._ctrl_conn: Optional[socket.socket] = None
        self._data_conn: Optional[socket.socket] = None

        self._threads: list[threading.Thread] = []

    def start(self) -> None:
        self._ctrl_server = self._bind_server(self.config.host, self.config.ctrl_port)
        self._data_server = self._bind_server(self.config.host, self.config.data_port)

        self._threads = [
            threading.Thread(target=self._control_accept_loop, name="radar-ctrl", daemon=True),
            threading.Thread(target=self._data_accept_loop, name="radar-data", daemon=True),
            threading.Thread(target=self._data_stream_loop, name="radar-stream", daemon=True),
        ]
        for thread in self._threads:
            thread.start()

        logger.info(
            "Radar emulator started on %s:%s (control) and %s:%s (data)",
            self.config.host,
            self.config.ctrl_port,
            self.config.host,
            self.config.data_port,
        )

    def stop(self, join_timeout_s: float = 1.0) -> None:
        self._stop_event.set()
        self._close_socket(self._ctrl_conn)
        self._close_socket(self._data_conn)
        self._close_socket(self._ctrl_server)
        self._close_socket(self._data_server)
        for thread in self._threads:
            thread.join(timeout=join_timeout_s)

    def serve_forever(self) -> None:
        self.start()
        try:
            while not self._stop_event.is_set():
                time.sleep(0.25)
        finally:
            self.stop()

    def __enter__(self) -> "RadarEmulator":
        self.start()
        return self

    def __exit__(self, exc_type, exc, tb) -> None:  # type: ignore[no-untyped-def]
        self.stop()

    @staticmethod
    def _bind_server(host: str, port: int) -> socket.socket:
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((host, port))
        server.listen(1)
        server.settimeout(0.25)
        return server

    @staticmethod
    def _close_socket(sock: Optional[socket.socket]) -> None:
        if sock is None:
            return
        try:
            sock.close()
        except OSError:
            pass

    @staticmethod
    def _recv_exact(sock: socket.socket, size: int) -> bytes:
        chunks: list[bytes] = []
        remaining = size
        while remaining > 0:
            chunk = sock.recv(remaining)
            if not chunk:
                raise ConnectionError("peer disconnected")
            chunks.append(chunk)
            remaining -= len(chunk)
        return b"".join(chunks)

    def _control_accept_loop(self) -> None:
        assert self._ctrl_server is not None
        while not self._stop_event.is_set():
            try:
                conn, addr = self._ctrl_server.accept()
            except socket.timeout:
                continue
            except OSError:
                break

            logger.info("Radar control client connected from %s:%s", *addr)
            conn.settimeout(0.5)
            self._close_socket(self._ctrl_conn)
            self._ctrl_conn = conn
            self._control_client_loop(conn)

    def _control_client_loop(self, conn: socket.socket) -> None:
        while not self._stop_event.is_set():
            try:
                command_bytes = self._recv_exact(conn, 4)
            except socket.timeout:
                continue
            except (ConnectionError, OSError):
                return

            command = command_bytes.decode("ascii", errors="replace")
            payload: Optional[int] = None
            if command == "#IRF":
                try:
                    payload = struct.unpack(">I", self._recv_exact(conn, 4))[0]
                except (ConnectionError, OSError, struct.error):
                    return

            ack, maybe_value = self._process_command(command, payload)
            try:
                conn.sendall(ack.encode("ascii"))
                if maybe_value is not None:
                    conn.sendall(struct.pack(">I", maybe_value))
            except OSError:
                return

    def _data_accept_loop(self) -> None:
        assert self._data_server is not None
        while not self._stop_event.is_set():
            try:
                conn, addr = self._data_server.accept()
            except socket.timeout:
                continue
            except OSError:
                break

            logger.info("Radar data client connected from %s:%s", *addr)
            conn.settimeout(0.5)
            self._close_socket(self._data_conn)
            self._data_conn = conn

    def _data_stream_loop(self) -> None:
        while not self._stop_event.is_set():
            with self._state_lock:
                should_stream = self._is_running
                frame_counter = self._frame_counter
                measurement_limit = self._measurement_limit

            if not should_stream:
                time.sleep(0.01)
                continue

            conn = self._data_conn
            if conn is None:
                time.sleep(0.01)
                continue

            frame = self._build_frame(frame_counter)
            try:
                conn.sendall(frame)
            except OSError:
                self._close_socket(conn)
                self._data_conn = None
                continue

            with self._state_lock:
                self._frame_counter += 1
                if measurement_limit > 0 and self._frame_counter >= measurement_limit:
                    self._is_running = False

            time.sleep(self.config.frame_interval_s)

    def _process_command(self, command: str, payload: Optional[int]) -> tuple[str, Optional[int]]:
        with self._state_lock:
            if command == "STA?":
                return "ACKN", self.config.status_flags
            if command == "FMT?":
                return "ACKN", 2
            if command == "IRF1":
                self._irf_enabled = True
                return "ACKN", None
            if command == "#IRF":
                self._measurement_limit = int(payload or 0)
                return "ACKN", None
            if command == "RUN ":
                self._is_running = True
                self._frame_counter = 0
                return "ACKN", None
            if command == "STOP":
                self._is_running = False
                return "ACKN", None

        logger.warning("Radar emulator received unsupported command %r", command)
        return "NACK", None

    def _build_frame(self, frame_counter: int) -> bytes:
        n = self.config.n_samples
        sample_bytes = self.config.sample_bytes
        if sample_bytes != 4:
            raise ValueError("Synapse expects 4-byte sample size")
        if self.config.header_fields * self.config.sample_bytes < 20:
            raise ValueError("Header must be at least 20 bytes for Synapse parsing")

        rx1 = [int(2200 * math.sin((frame_counter + i) / 17.0)) for i in range(n)]
        rx2 = [int(2100 * math.cos((frame_counter + i) / 19.0)) for i in range(n)]

        payload = struct.pack(f"<{n}i", *rx1) + struct.pack(f"<{n}i", *rx2)

        header = bytearray(self.config.header_fields * self.config.sample_bytes)
        struct.pack_into("<I", header, 0, frame_counter)
        struct.pack_into("<I", header, 4, self.config.adc1_level)
        struct.pack_into("<I", header, 8, self.config.adc2_level)
        struct.pack_into("<f", header, 12, self.config.cpu_temp_c)
        struct.pack_into("<f", header, 16, self.config.board_temp_c)

        return payload + bytes(header)
