from __future__ import annotations

import collections
import json
import socket
import struct
import threading
import time
from dataclasses import dataclass
from typing import Any, Callable, Deque, Dict, Optional, Tuple
from urllib.error import URLError
from urllib.parse import urlsplit
from urllib.request import Request, urlopen

from ..core.bus import CANBusEmulation, CANBusParticipant
from ..core.can import CANFrame

try:
    from zeroconf import Zeroconf
except Exception:  # pragma: no cover - optional dependency
    Zeroconf = None  # type: ignore[assignment]


ENET_SERVICE_BROKER_DEFAULT_URL = ""
ENET_SERVICE_BROKER_DEFAULT_MODE = "isotp"
ENET_SERVICE_BROKER_SUPPORTED_MODES = {"raw", "isotp"}
ENET_SERVICE_BROKER_ZEROCONF_SERVICE_NAME = "enet-service-broker._enetbroker._tcp"
ENET_SERVICE_BROKER_ZEROCONF_TIMEOUT_S = 2.0

_PROTOCOL_VERSION = 1
_MODE_RAW = 0
_MODE_ISOTP = 1
_HEADER_FORMAT = ">BBBBIHQ"
_HEADER_SIZE = struct.calcsize(_HEADER_FORMAT)
_MAX_PACKET_LENGTH = 0xFFFF
_MAX_RAW_LENGTH = 8
_MAX_ISOTP_LENGTH = 0xFFF
_ISOTP_TIMEOUT_S = 1.5
_ISOTP_UNICAST_PAIR_TIMEOUT_S = 30.0


@dataclass(frozen=True)
class BrokerEndpoint:
    base_url: str
    stream_host: str


class BrokerDiscoveryError(RuntimeError):
    pass


@dataclass
class IsoTpRxSession:
    expected_len: int
    data: bytearray
    next_sn: int
    is_extended: bool
    last_activity: float


@dataclass
class IsoTpTxSession:
    can_id: int
    is_extended: bool
    payload: bytes
    offset: int
    next_sn: int
    waiting_for_fc: bool
    block_size: int
    block_remaining: int
    separation_time_s: float
    expected_fc_id: int
    flow_control_source_locked: bool
    flow_control_source_id: int
    last_activity: float


class ENETServiceBrokerParticipant(CANBusParticipant):
    def __init__(self, service: "ENETServiceBrokerService") -> None:
        super().__init__()
        self._service = service

    @property
    def arbitration_ids(self):
        return set()

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        self._service._handle_bus_frame(frame)


class IsoTpBridge:
    def __init__(
        self,
        send_broker_packet: Callable[[int, bool, bytes], None],
        write_can_frame: Callable[[int, bool, bytes], None],
    ) -> None:
        self.send_broker_packet = send_broker_packet
        self.write_can_frame = write_can_frame
        self.lock = threading.Lock()
        self.rx_sessions: Dict[int, IsoTpRxSession] = {}
        self.tx_active: Dict[int, IsoTpTxSession] = {}
        self.tx_queues: Dict[int, Deque[Tuple[bool, bytes]]] = collections.defaultdict(collections.deque)
        self.unicast_pairs: Dict[int, Tuple[int, float]] = {}
        self.last_outbound_can_id: Optional[int] = None
        self.last_outbound_at: float = 0.0

    def handle_local_frame(self, frame: CANFrame) -> None:
        data = bytes(frame.data[: frame.dlc])
        if not data:
            return

        can_id = int(frame.arbitration_id)
        is_extended = can_id > 0x7FF

        with self.lock:
            self._cleanup_stale()
            pci_type = data[0] & 0xF0
            if pci_type == 0x30:
                self._handle_flow_control(can_id, data)
                return

            if pci_type == 0x00:
                self._handle_single_frame(can_id, is_extended, data)
                return

            if pci_type == 0x10:
                self._handle_first_frame(can_id, is_extended, data)
                return

            if pci_type == 0x20:
                self._handle_consecutive_frame(can_id, data)
                return

    def enqueue_remote_pdu(self, can_id: int, is_extended: bool, payload: bytes) -> None:
        if not payload:
            return
        if len(payload) > _MAX_ISOTP_LENGTH:
            return

        with self.lock:
            self._cleanup_stale()
            self.tx_queues[can_id].append((is_extended, payload))
            if can_id not in self.tx_active:
                self._start_next_tx(can_id)

    def _cleanup_stale(self) -> None:
        now = time.monotonic()

        for can_id in list(self.rx_sessions.keys()):
            if now - self.rx_sessions[can_id].last_activity > _ISOTP_TIMEOUT_S:
                del self.rx_sessions[can_id]

        for can_id in list(self.tx_active.keys()):
            sess = self.tx_active[can_id]
            if now - sess.last_activity <= _ISOTP_TIMEOUT_S:
                continue
            del self.tx_active[can_id]
            self._start_next_tx(can_id)

        for can_id in list(self.unicast_pairs.keys()):
            _peer_id, last_seen = self.unicast_pairs[can_id]
            if now - last_seen > _ISOTP_UNICAST_PAIR_TIMEOUT_S:
                del self.unicast_pairs[can_id]

    def _emit_complete_pdu(self, can_id: int, is_extended: bool, payload: bytes) -> None:
        self.send_broker_packet(can_id, is_extended, payload)

    def _handle_single_frame(self, can_id: int, is_extended: bool, data: bytes) -> None:
        length = data[0] & 0x0F
        if length > 7 or len(data) < 1 + length:
            return
        payload = data[1 : 1 + length]
        if payload:
            self._emit_complete_pdu(can_id, is_extended, payload)

    def _handle_first_frame(self, can_id: int, is_extended: bool, data: bytes) -> None:
        if len(data) < 2:
            return
        total_len = ((data[0] & 0x0F) << 8) | data[1]
        if total_len <= 7 or total_len > _MAX_ISOTP_LENGTH:
            return

        payload = bytearray(data[2:])
        self.rx_sessions[can_id] = IsoTpRxSession(
            expected_len=total_len,
            data=payload,
            next_sn=1,
            is_extended=is_extended,
            last_activity=time.monotonic(),
        )

        fc_id = self._resolve_flow_control_target(can_id)
        self._remember_pair(can_id, fc_id)
        self.write_can_frame(fc_id, fc_id > 0x7FF, bytes([0x30, 0x00, 0x00]))

        if len(payload) >= total_len:
            final_payload = bytes(payload[:total_len])
            del self.rx_sessions[can_id]
            self._emit_complete_pdu(can_id, is_extended, final_payload)

    def _handle_consecutive_frame(self, can_id: int, data: bytes) -> None:
        sess = self.rx_sessions.get(can_id)
        if not sess:
            return

        sn = data[0] & 0x0F
        if sn != sess.next_sn:
            del self.rx_sessions[can_id]
            return

        sess.data.extend(data[1:])
        sess.next_sn = (sess.next_sn + 1) & 0x0F
        sess.last_activity = time.monotonic()

        if len(sess.data) >= sess.expected_len:
            payload = bytes(sess.data[: sess.expected_len])
            is_extended = sess.is_extended
            del self.rx_sessions[can_id]
            self._emit_complete_pdu(can_id, is_extended, payload)

    def _start_next_tx(self, can_id: int) -> None:
        queue = self.tx_queues.get(can_id)
        if not queue:
            self.tx_queues.pop(can_id, None)
            return

        is_extended, payload = queue.popleft()
        if not queue:
            self.tx_queues.pop(can_id, None)

        expected_fc_id = self._known_peer_for(can_id)
        if expected_fc_id is None:
            expected_fc_id = expected_flow_control_source(can_id)
        self.last_outbound_can_id = can_id
        self.last_outbound_at = time.monotonic()

        if len(payload) <= 7:
            sf = bytes([len(payload)]) + payload
            self.write_can_frame(can_id, is_extended, sf)
            self._start_next_tx(can_id)
            return

        first_payload = payload[:6]
        ff = bytes([0x10 | ((len(payload) >> 8) & 0x0F), len(payload) & 0xFF]) + first_payload
        self.write_can_frame(can_id, is_extended, ff)

        self.tx_active[can_id] = IsoTpTxSession(
            can_id=can_id,
            is_extended=is_extended,
            payload=payload,
            offset=len(first_payload),
            next_sn=1,
            waiting_for_fc=True,
            block_size=0,
            block_remaining=0,
            separation_time_s=0.0,
            expected_fc_id=expected_fc_id,
            flow_control_source_locked=False,
            flow_control_source_id=expected_fc_id,
            last_activity=time.monotonic(),
        )

    def _handle_flow_control(self, fc_can_id: int, data: bytes) -> None:
        status = data[0] & 0x0F
        block_size = data[1] if len(data) > 1 else 0
        stmin = data[2] if len(data) > 2 else 0

        selected = self._select_tx_session_for_flow_control(fc_can_id)
        if selected is None:
            return
        can_id, sess = selected

        sess.last_activity = time.monotonic()
        if not sess.flow_control_source_locked:
            sess.flow_control_source_locked = True
            sess.flow_control_source_id = fc_can_id

        if status == 0x00:
            self._remember_pair(can_id, fc_can_id)
            sess.block_size = block_size
            sess.block_remaining = block_size
            sess.separation_time_s = stmin_to_seconds(stmin)
            sess.waiting_for_fc = False
            self._send_cf_burst(sess)
        elif status == 0x01:
            self._remember_pair(can_id, fc_can_id)
        else:
            del self.tx_active[can_id]
            self._start_next_tx(can_id)
        return

    def _send_cf_burst(self, sess: IsoTpTxSession) -> None:
        while not sess.waiting_for_fc and sess.offset < len(sess.payload):
            chunk = sess.payload[sess.offset : sess.offset + 7]
            cf = bytes([0x20 | (sess.next_sn & 0x0F)]) + chunk
            self.write_can_frame(sess.can_id, sess.is_extended, cf)

            sess.offset += len(chunk)
            sess.next_sn = (sess.next_sn + 1) & 0x0F
            sess.last_activity = time.monotonic()

            if sess.block_size > 0:
                sess.block_remaining -= 1
                if sess.block_remaining == 0 and sess.offset < len(sess.payload):
                    sess.waiting_for_fc = True
                    break

            if sess.separation_time_s > 0:
                time.sleep(sess.separation_time_s)

        if sess.offset >= len(sess.payload):
            can_id = sess.can_id
            del self.tx_active[can_id]
            self._start_next_tx(can_id)

    def _known_peer_for(self, can_id: int) -> Optional[int]:
        pair = self.unicast_pairs.get(can_id)
        if pair is None:
            return None
        peer_id, _last_seen = pair
        return peer_id

    def _remember_pair(self, can_id: int, peer_id: int) -> None:
        if can_id == peer_id:
            return
        now = time.monotonic()
        self.unicast_pairs[can_id] = (peer_id, now)
        self.unicast_pairs[peer_id] = (can_id, now)

    def _select_tx_session_for_flow_control(self, fc_can_id: int) -> Optional[Tuple[int, IsoTpTxSession]]:
        for can_id, sess in self.tx_active.items():
            if not sess.waiting_for_fc:
                continue
            if not sess.flow_control_source_locked:
                continue
            if sess.flow_control_source_id == fc_can_id:
                return can_id, sess

        for can_id, sess in self.tx_active.items():
            if not sess.waiting_for_fc:
                continue
            if sess.flow_control_source_locked:
                continue
            if sess.flow_control_source_id == fc_can_id:
                return can_id, sess

        peer_id = self._known_peer_for(fc_can_id)
        if peer_id is not None:
            sess = self.tx_active.get(peer_id)
            if sess is not None and sess.waiting_for_fc and not sess.flow_control_source_locked:
                return peer_id, sess

        single_unbound: Optional[Tuple[int, IsoTpTxSession]] = None
        unbound_count = 0
        for can_id, sess in self.tx_active.items():
            if not sess.waiting_for_fc:
                continue
            if sess.flow_control_source_locked:
                continue
            unbound_count += 1
            if unbound_count == 1:
                single_unbound = (can_id, sess)
        if unbound_count == 1:
            return single_unbound
        return None

    def _resolve_flow_control_target(self, responder_id: int) -> int:
        peer_id = self._known_peer_for(responder_id)
        if peer_id is not None:
            return peer_id

        active_tx_ids = list(self.tx_active.keys())
        if len(active_tx_ids) == 1:
            return active_tx_ids[0]

        if self.last_outbound_can_id is not None and (time.monotonic() - self.last_outbound_at) <= _ISOTP_TIMEOUT_S:
            return self.last_outbound_can_id

        return flow_control_target(responder_id)


def _split_broker_value(value: str) -> tuple[str, Optional[int], str]:
    if "://" in value:
        parsed = urlsplit(value)
        scheme = parsed.scheme.lower()
    else:
        parsed = urlsplit(f"//{value}")
        scheme = "https"

    if scheme not in ("http", "https"):
        scheme = "https"

    host = parsed.hostname or ""
    port = parsed.port
    return scheme, port, host


def resolve_broker_endpoint(value: Optional[str]) -> BrokerEndpoint:
    raw = (value or "").strip()
    if not raw:
        raise ValueError("ENET Service Broker URL override is empty")
    scheme, port, host = _split_broker_value(raw)
    if not host:
        raise ValueError(f"Invalid ENET Service Broker URL override: {raw!r}")
    host_authority = f"[{host}]" if ":" in host and not host.startswith("[") else host
    authority = f"{host_authority}:{port}" if port else host_authority
    return BrokerEndpoint(base_url=f"{scheme}://{authority}", stream_host=host)


def _normalize_zeroconf_service_name(value: str) -> tuple[str, str]:
    raw = (value or "").strip().rstrip(".")
    if not raw:
        raise BrokerDiscoveryError("Zeroconf service name is empty")
    parts = [part for part in raw.split(".") if part]
    if len(parts) < 3:
        raise BrokerDiscoveryError(
            f"Invalid Zeroconf service name '{value}'. Expected '<instance>.<service>._tcp'."
        )
    service_type = ".".join(parts[-2:])
    full_name = ".".join(parts)
    return f"{service_type}.local.", f"{full_name}.local."


def _decode_service_properties(raw: Any) -> dict[str, str]:
    if not isinstance(raw, dict):
        return {}
    decoded: dict[str, str] = {}
    for key, value in raw.items():
        if isinstance(key, bytes):
            key_text = key.decode("utf-8", errors="ignore")
        else:
            key_text = str(key)
        if isinstance(value, bytes):
            value_text = value.decode("utf-8", errors="ignore")
        else:
            value_text = str(value)
        if key_text:
            decoded[key_text.strip().lower()] = value_text.strip()
    return decoded


def _first_service_address(info: Any) -> Optional[str]:
    if hasattr(info, "parsed_addresses"):
        try:
            parsed = info.parsed_addresses()
        except TypeError:
            try:
                parsed = info.parsed_addresses(None)
            except Exception:
                parsed = []
        except Exception:
            parsed = []
        if parsed:
            return str(parsed[0]).split("%", 1)[0]

    for packed in getattr(info, "addresses", []) or []:
        try:
            if len(packed) == 4:
                return socket.inet_ntop(socket.AF_INET, packed)
            if len(packed) == 16:
                return socket.inet_ntop(socket.AF_INET6, packed)
        except Exception:
            continue

    server = getattr(info, "server", "")
    if isinstance(server, str) and server.strip():
        return server.strip().rstrip(".")
    return None


def discover_broker_endpoint(
    service_name: str = ENET_SERVICE_BROKER_ZEROCONF_SERVICE_NAME,
    timeout_s: float = ENET_SERVICE_BROKER_ZEROCONF_TIMEOUT_S,
) -> BrokerEndpoint:
    if Zeroconf is None:
        raise BrokerDiscoveryError(
            "Zeroconf discovery requires the 'zeroconf' package. "
            "Install zeroconf or set an ENET Service Broker URL override."
        )

    service_type, full_name = _normalize_zeroconf_service_name(service_name)
    timeout_ms = max(100, int(max(timeout_s, 0.1) * 1000))
    zc = Zeroconf()
    try:
        info = zc.get_service_info(service_type, full_name, timeout=timeout_ms)
    finally:
        zc.close()

    if info is None:
        raise BrokerDiscoveryError(
            f"Zeroconf service '{service_name}' was not found on the local network."
        )

    host = _first_service_address(info)
    if not host:
        raise BrokerDiscoveryError(
            f"Zeroconf service '{service_name}' did not provide a reachable host."
        )

    try:
        port = int(getattr(info, "port", 0))
    except (TypeError, ValueError):
        port = 0
    if port <= 0:
        raise BrokerDiscoveryError(
            f"Zeroconf service '{service_name}' did not provide a valid HTTP port."
        )

    properties = _decode_service_properties(getattr(info, "properties", {}))
    scheme = properties.get("scheme", "").strip().lower()
    if scheme not in {"http", "https"}:
        tls_value = properties.get("tls", "").strip().lower()
        scheme = "https" if tls_value in {"1", "true", "yes", "on"} else "http"

    host_authority = f"[{host}]" if ":" in host and not host.startswith("[") else host
    authority = f"{host_authority}:{port}"
    return BrokerEndpoint(base_url=f"{scheme}://{authority}", stream_host=host)


def _normalize_stream_host(value: Optional[str], fallback_host: str) -> str:
    candidate = (value or "").strip()
    if not candidate:
        return fallback_host
    _scheme, _port, host = _split_broker_value(candidate)
    return host or fallback_host


def _resolve_host_name(host: Optional[str]) -> Optional[str]:
    candidate = (host or "").strip()
    if not candidate:
        return None
    try:
        resolved, _aliases, _addresses = socket.gethostbyaddr(candidate)
    except Exception:
        return None
    text = str(resolved).strip()
    if not text:
        return None
    return text.rstrip(".")


def _format_network_error(exc: BaseException) -> str:
    reason = getattr(exc, "reason", None)
    if isinstance(reason, OSError):
        detail = reason.strerror or str(reason)
        if detail:
            return detail
    if isinstance(reason, str) and reason.strip():
        return reason.strip()
    text = str(exc).strip()
    return text or exc.__class__.__name__


def _sanitize_vin(vin: str) -> str:
    return "".join(ch for ch in vin.upper() if ch.isalnum())[:17]


def _normalize_mode(mode: str) -> str:
    normalized = (mode or "").strip().lower() or ENET_SERVICE_BROKER_DEFAULT_MODE
    if normalized not in ENET_SERVICE_BROKER_SUPPORTED_MODES:
        supported = ", ".join(sorted(ENET_SERVICE_BROKER_SUPPORTED_MODES))
        raise ValueError(f"Unsupported ENET Service Broker mode '{mode}'. Supported: {supported}")
    return normalized


def _mode_to_byte(mode: str) -> int:
    return _MODE_ISOTP if mode == "isotp" else _MODE_RAW


def flow_control_target(can_id: int) -> int:
    if can_id > 0x7FF:
        high = can_id & 0xFFFF0000
        src = (can_id >> 8) & 0xFF
        dst = can_id & 0xFF
        return high | (dst << 8) | src
    if 0x7E0 <= can_id <= 0x7E7:
        return can_id + 0x08
    if 0x7E8 <= can_id <= 0x7EF:
        return can_id - 0x08
    if can_id <= 0x7F7:
        return can_id + 0x08
    return can_id


def expected_flow_control_source(can_id: int) -> int:
    if can_id > 0x7FF:
        high = can_id & 0xFFFF0000
        src = (can_id >> 8) & 0xFF
        dst = can_id & 0xFF
        return high | (dst << 8) | src
    if 0x7E0 <= can_id <= 0x7E7:
        return can_id + 0x08
    if 0x7E8 <= can_id <= 0x7EF:
        return can_id - 0x08
    if can_id <= 0x7F7:
        return can_id + 0x08
    return can_id


def stmin_to_seconds(stmin: int) -> float:
    if stmin <= 0x7F:
        return stmin / 1000.0
    if 0xF1 <= stmin <= 0xF9:
        return (stmin - 0xF0) / 10000.0
    return 0.0


class ENETServiceBrokerService:
    def __init__(
        self,
        bus: CANBusEmulation,
        vin: str,
        broker_url: str = ENET_SERVICE_BROKER_DEFAULT_URL,
        mode: str = ENET_SERVICE_BROKER_DEFAULT_MODE,
        log: bool = False,
        event_sink: Optional[Callable[[dict[str, Any]], None]] = None,
    ) -> None:
        sanitized_vin = _sanitize_vin(vin)
        if len(sanitized_vin) != 17:
            raise ValueError("ENET Service Broker requires a 17-character VIN")
        self.bus = bus
        self.vin = sanitized_vin
        self.broker_url = (broker_url or "").strip()
        self.mode = _normalize_mode(mode)
        self._mode_byte = _mode_to_byte(self.mode)
        self.log = log
        self._event_sink = event_sink

        self._stop = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._socket: Optional[socket.socket] = None
        self._socket_lock = threading.Lock()
        self._write_lock = threading.Lock()
        self._participant = ENETServiceBrokerParticipant(self)
        self._isotp_bridge: Optional[IsoTpBridge] = None

        self._last_rx: Optional[float] = None
        self._last_tx: Optional[float] = None
        self._rx_bytes = 0
        self._tx_bytes = 0
        self._connected_peer: Optional[str] = None
        self._vehicle_port: Optional[int] = None
        self._tester_port: Optional[int] = None
        self._broker_host: Optional[str] = None
        self._broker_name: Optional[str] = None
        self._last_error: Optional[str] = None

    def _log(self, message: str) -> None:
        if self.log:
            print(message)

    def _emit_event(self, kind: str, **payload: Any) -> None:
        if self._event_sink is None:
            return
        event_payload: dict[str, Any] = {"kind": kind, "timestamp": time.time()}
        event_payload.update(payload)
        try:
            self._event_sink(event_payload)
        except Exception:
            pass

    def _set_error(self, message: str) -> None:
        text = str(message).strip()
        with self._socket_lock:
            self._last_error = text or "Unknown ENET Service Broker error"

    def _clear_error(self) -> None:
        with self._socket_lock:
            self._last_error = None

    def _report_connection_error(self, peer: str, message: str) -> None:
        self._set_error(message)
        self._emit_event("connection_error", peer=peer, message=message)

    def _resolve_endpoint(self) -> BrokerEndpoint:
        override = (self.broker_url or "").strip()
        if override:
            return resolve_broker_endpoint(override)
        return discover_broker_endpoint()

    def start(self) -> bool:
        if self._thread and self._thread.is_alive():
            return True
        self._stop.clear()
        self._clear_error()
        self._vehicle_port = None
        self._tester_port = None
        self._broker_host = None
        self._broker_name = None
        self.bus.attach(self._participant)
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        endpoint_label = (
            self.broker_url
            if self.broker_url
            else f"zeroconf:{ENET_SERVICE_BROKER_ZEROCONF_SERVICE_NAME}"
        )
        print(
            f"DEBUG: ENET Service Broker bridge started with VIN {self.vin} @ {endpoint_label} (mode={self.mode})",
            flush=True,
        )
        return True

    def stop(self) -> None:
        self._stop.set()
        with self._socket_lock:
            self._close_socket_locked()
            self._connected_peer = None
            self._broker_host = None
            self._broker_name = None
            self._last_error = None
        self._isotp_bridge = None
        self.bus.detach(self._participant)
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        print("DEBUG: ENET Service Broker bridge stopped", flush=True)

    def ui_snapshot(self) -> dict[str, Any]:
        connected = False
        broker_host: Optional[str] = None
        broker_name: Optional[str] = None
        last_error: Optional[str] = None
        with self._socket_lock:
            connected = self._socket is not None
            broker_host = self._broker_host
            broker_name = self._broker_name
            last_error = self._last_error
        return {
            "connected": connected,
            "clients": 1 if connected else 0,
            "last_rx": self._last_rx,
            "last_tx": self._last_tx,
            "rx_bytes": self._rx_bytes,
            "tx_bytes": self._tx_bytes,
            "tester_port": self._tester_port,
            "broker_host": broker_host,
            "broker_name": broker_name,
            "mode": self.mode,
            "error": last_error,
        }

    def _request_ports(self, endpoint: BrokerEndpoint) -> Optional[tuple[str, int, int]]:
        url = f"{endpoint.base_url}/port/{self.vin}"
        request = Request(url, method="POST")
        try:
            with urlopen(request, timeout=10.0) as response:
                status = getattr(response, "status", 200)
                payload = response.read().decode("utf-8", errors="replace")
        except URLError as exc:
            message = f"Port request failed: {_format_network_error(exc)}"
            self._report_connection_error(endpoint.base_url, message)
            self._log(f"ENET broker request failed: {message}")
            return None

        if status != 200:
            message = f"Port request failed with HTTP {status}"
            self._report_connection_error(endpoint.base_url, message)
            self._log(f"ENET broker returned HTTP {status}")
            return None

        try:
            data = json.loads(payload)
        except json.JSONDecodeError:
            self._report_connection_error(endpoint.base_url, "Invalid broker JSON response")
            self._log("ENET broker returned invalid JSON")
            return None

        vehicle = data.get("vehicle") or {}
        tester = data.get("tester") or {}
        ip = _normalize_stream_host(vehicle.get("ip") or tester.get("ip"), endpoint.stream_host)

        try:
            vehicle_port = int(vehicle.get("port", 0))
            tester_port = int(tester.get("port", 0))
        except (TypeError, ValueError):
            vehicle_port = 0
            tester_port = 0

        if vehicle_port <= 0 or tester_port <= 0:
            self._report_connection_error(endpoint.base_url, "Broker response missing vehicle/tester ports")
            self._log("ENET broker response missing ports")
            return None

        self._clear_error()
        return ip, vehicle_port, tester_port

    def _run(self) -> None:
        while not self._stop.is_set():
            try:
                endpoint = self._resolve_endpoint()
                resolved_name = _resolve_host_name(endpoint.stream_host)
                with self._socket_lock:
                    self._broker_host = endpoint.stream_host or None
                    self._broker_name = resolved_name
            except (ValueError, BrokerDiscoveryError) as exc:
                with self._socket_lock:
                    self._broker_host = None
                    self._broker_name = None
                peer = self.broker_url or f"zeroconf:{ENET_SERVICE_BROKER_ZEROCONF_SERVICE_NAME}"
                self._report_connection_error(peer, str(exc))
                self._sleep_with_stop(1.0)
                continue
            port_info = self._request_ports(endpoint)
            if port_info is None:
                self._sleep_with_stop(1.0)
                continue

            ip, vehicle_port, tester_port = port_info
            peer = f"{ip}:{vehicle_port}"
            self._vehicle_port = vehicle_port
            self._tester_port = tester_port
            resolved_name = _resolve_host_name(ip)
            with self._socket_lock:
                self._broker_host = ip
                self._broker_name = resolved_name
            self._log(
                f"ENET broker allocated VIN {self.vin}: vehicle={vehicle_port} tester={tester_port} host={ip}"
            )
            sock: Optional[socket.socket] = None
            try:
                sock = socket.create_connection((ip, vehicle_port), timeout=10.0)
                sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                sock.settimeout(0.5)
                with self._socket_lock:
                    self._socket = sock
                    self._connected_peer = peer
                    self._last_error = None
                if self.mode == "isotp":
                    self._isotp_bridge = IsoTpBridge(
                        send_broker_packet=lambda can_id, is_extended, payload: self._send_broker_packet(
                            can_id, is_extended, _MODE_ISOTP, payload
                        ),
                        write_can_frame=self._inject_isotp_can_frame,
                    )
                else:
                    self._isotp_bridge = None
                self._emit_event("connection_opened", peer=peer)
                close_reason = self._bridge_connection(sock)
                self._emit_event("connection_closed", peer=peer, message=close_reason)
            except OSError as exc:
                message = f"TCP connect failed: {exc}"
                self._report_connection_error(peer, message)
                self._log(f"ENET vehicle connect failed: {exc}")
            finally:
                with self._socket_lock:
                    if self._socket is sock:
                        self._close_socket_locked()
                    self._connected_peer = None
                self._isotp_bridge = None
                if not self._stop.is_set():
                    self._sleep_with_stop(0.5)

    def _bridge_connection(self, sock: socket.socket) -> str:
        while not self._stop.is_set():
            try:
                packet = self._read_packet(sock)
            except ValueError as exc:
                self._report_connection_error(self._connected_peer or "unknown", str(exc))
                return str(exc)

            if packet is None:
                return "Connection dropped"

            _ts_us, can_id, is_extended, mode, payload = packet
            if mode != self._mode_byte:
                return (
                    f"Broker packet mode mismatch: got {mode}, expected {self._mode_byte} "
                    f"for configured mode '{self.mode}'"
                )

            self._last_rx = time.time()
            self._rx_bytes += _HEADER_SIZE + len(payload)

            if mode == _MODE_RAW:
                dlc = min(len(payload), _MAX_RAW_LENGTH)
                frame = CANFrame(arbitration_id=can_id, data=list(payload[:dlc]), dlc=dlc)
                self.bus.write(frame, originator=self._participant)
                continue

            bridge = self._isotp_bridge
            if bridge is None:
                return "ISOTP bridge not initialized"
            bridge.enqueue_remote_pdu(can_id=can_id, is_extended=is_extended, payload=payload)

        return "Stopped"

    def _read_packet(self, sock: socket.socket) -> Optional[tuple[int, int, bool, int, bytes]]:
        header = self._read_exact(sock, _HEADER_SIZE)
        if not header:
            return None
        version, mode, flags, _reserved, can_id, length, ts_us = struct.unpack(_HEADER_FORMAT, header)
        if version != _PROTOCOL_VERSION:
            raise ValueError(f"Unsupported broker protocol version {version}")
        if length < 0 or length > _MAX_PACKET_LENGTH:
            raise ValueError(f"Invalid broker packet length {length}")

        payload = self._read_exact(sock, length)
        if payload is None:
            return None

        if mode == _MODE_RAW:
            payload = payload[:_MAX_RAW_LENGTH]
        elif mode == _MODE_ISOTP:
            payload = payload[:_MAX_ISOTP_LENGTH]

        return ts_us, can_id, bool(flags & 0x01), mode, payload

    def _read_exact(self, sock: socket.socket, length: int) -> Optional[bytes]:
        if length == 0:
            return b""
        chunks: list[bytes] = []
        remaining = length
        while remaining > 0 and not self._stop.is_set():
            try:
                chunk = sock.recv(remaining)
            except socket.timeout:
                continue
            except OSError:
                return None
            if not chunk:
                return None
            chunks.append(chunk)
            remaining -= len(chunk)
        if remaining > 0:
            return None
        return b"".join(chunks)

    def _handle_bus_frame(self, frame: CANFrame) -> None:
        if self._stop.is_set():
            return

        if self.mode == "raw":
            payload = bytes(frame.data[: frame.dlc])
            self._send_broker_packet(
                can_id=frame.arbitration_id,
                is_extended=frame.arbitration_id > 0x7FF,
                mode=_MODE_RAW,
                payload=payload,
            )
            return

        bridge = self._isotp_bridge
        if bridge is None:
            return
        bridge.handle_local_frame(frame)

    def _send_broker_packet(self, can_id: int, is_extended: bool, mode: int, payload: bytes) -> None:
        packet = struct.pack(
            _HEADER_FORMAT,
            _PROTOCOL_VERSION,
            mode,
            1 if is_extended else 0,
            0,
            can_id,
            len(payload),
            int(time.time() * 1_000_000),
        ) + payload

        sock: Optional[socket.socket] = None
        with self._socket_lock:
            sock = self._socket
        if sock is None:
            return

        with self._write_lock:
            try:
                sock.sendall(packet)
            except OSError as exc:
                self._report_connection_error(self._connected_peer or "unknown", f"TCP send failed: {exc}")
                with self._socket_lock:
                    if self._socket is sock:
                        self._close_socket_locked()
                return

        self._last_tx = time.time()
        self._tx_bytes += len(packet)

    def _inject_isotp_can_frame(self, can_id: int, is_extended: bool, payload: bytes) -> None:
        padded_payload = payload[:_MAX_RAW_LENGTH].ljust(_MAX_RAW_LENGTH, b"\x00")
        frame = CANFrame(
            arbitration_id=can_id,
            data=list(padded_payload),
            dlc=_MAX_RAW_LENGTH,
        )
        self.bus.write(frame, originator=self._participant)

    def _close_socket_locked(self) -> None:
        if self._socket is None:
            return
        try:
            self._socket.shutdown(socket.SHUT_RDWR)
        except OSError:
            pass
        try:
            self._socket.close()
        except OSError:
            pass
        self._socket = None

    def _sleep_with_stop(self, duration: float) -> None:
        deadline = time.monotonic() + max(duration, 0.0)
        while not self._stop.is_set() and time.monotonic() < deadline:
            time.sleep(0.05)
