from __future__ import annotations

import queue
import socket
import threading
import time
from dataclasses import dataclass
from typing import Any, Callable, Optional

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


DOIP_VERSION = 0x02
DOIP_INVERSE_VERSION = 0xFF - DOIP_VERSION
DOIP_PORT_TCP = 13400
DOIP_PORT_UDP = 13400
DOIP_PAYLOAD_VEHICLE_IDENT_REQUEST = 0x0001
DOIP_PAYLOAD_VEHICLE_IDENT_REQUEST_EID = 0x0002
DOIP_PAYLOAD_VEHICLE_IDENT_REQUEST_VIN = 0x0003
DOIP_PAYLOAD_VEHICLE_IDENT_RESPONSE = 0x0004
DOIP_PAYLOAD_ROUTING_ACTIVATION_REQUEST = 0x0005
DOIP_PAYLOAD_ROUTING_ACTIVATION_RESPONSE = 0x0006
DOIP_PAYLOAD_DIAGNOSTIC_MESSAGE = 0x8001
DOIP_PAYLOAD_DIAGNOSTIC_ACK = 0x8002
DOIP_PAYLOAD_DIAGNOSTIC_NACK = 0x8003

DOIP_BROADCAST_ADDRESS = 0xE400
DOIP_TESTER_ADDRESS = 0x0E80
DOIP_ROUTING_SOURCE_ADDRESS = 0x3828
DOIP_ROUTING_INVALID_SOURCE = 0x00
DOIP_ROUTING_SUCCESS = 0x10
DOIP_DIAGNOSTIC_NACK_UNKNOWN_TARGET = 0x03
DOIP_BROADCAST_UDP = "255.255.255.255"
DOIP_BROADCAST_LINK_LOCAL_UDP = "169.254.255.255"


@dataclass
class DoIPMessage:
    version: int
    inverse_version: int
    payload_type: int
    payload: bytes

    @property
    def payload_length(self) -> int:
        return len(self.payload)

    @property
    def bytes(self) -> bytes:
        header = _build_header(self.payload_type, self.payload_length)
        return header + self.payload

    @staticmethod
    def from_socket(client: socket.socket) -> Optional["DoIPMessage"]:
        header = _read_exact(client, 8)
        if not header:
            return None
        version = header[0]
        inverse_version = header[1]
        payload_type = (header[2] << 8) | header[3]
        payload_length = int.from_bytes(header[4:8], "big")
        if payload_length < 0:
            return None
        payload = _read_exact(client, payload_length) if payload_length else b""
        if payload_length and not payload:
            return None
        return DoIPMessage(
            version=version,
            inverse_version=inverse_version,
            payload_type=payload_type,
            payload=payload,
        )


class DoIPParticipant(CANBusParticipant):
    def __init__(self, server: "DoIPServer") -> None:
        super().__init__()
        self.server = server

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

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        self.server.handle_incoming_frame(frame)


class DoIPRequest:
    def __init__(self, source: int, destination: int, payload: list[int]) -> None:
        self.source = source
        self.destination = destination
        self.payload = payload
        self.is_broadcast = destination == DOIP_BROADCAST_ADDRESS
        self.tx_isotp = ISOTPTransceiver(block_size=0, separation_time_ms=1)
        self.rx_isotp: dict[int, ISOTPTransceiver] = {}
        self.responses: "queue.Queue[tuple[int, list[int]]]" = queue.Queue()

    def handle_tx_flow_control(self, frame: CANFrame) -> Optional[list[list[int]]]:
        if self.tx_isotp.state != "sending":
            return None
        try:
            action = self.tx_isotp.did_read(list(frame.data[:frame.dlc]))
        except Exception:
            return None
        if action.kind != "write_frames" or action.frames is None:
            return None
        return action.frames

    def handle_rx_frame(self, frame: CANFrame) -> Optional[list[list[int]]]:
        transceiver = self.rx_isotp.get(frame.arbitration_id)
        payload = list(frame.data[:frame.dlc])
        if transceiver is None or transceiver.frame_length != len(payload):
            transceiver = ISOTPTransceiver(block_size=0, separation_time_ms=1, frame_length=len(payload))
            self.rx_isotp[frame.arbitration_id] = transceiver
        try:
            action = transceiver.did_read(payload)
        except Exception:
            return None
        if action.kind == "process" and action.bytes is not None:
            self.responses.put((frame.arbitration_id, action.bytes))
            return None
        if action.kind == "write_frames" and action.frames is not None:
            return action.frames
        return None


class DoIPServer:
    def __init__(
        self,
        bus: CANBusEmulation,
        host: str,
        port: int,
        log: bool = False,
        vin: str = "WBA45678901234567",
        tester_address: Optional[int] = None,
        event_sink: Optional[Callable[[dict[str, Any]], None]] = None,
    ) -> None:
        self.bus = bus
        self.host = host
        self.port = port
        self.log = log
        self._event_sink = event_sink
        self.vin = _sanitize_vin(vin)
        if tester_address is not None and not (0 <= tester_address <= 0xFFFF):
            raise ValueError("DoIP tester address must be between 0x0000 and 0xFFFF")
        self.tester_address = tester_address
        self.vehicle_announcement = _build_vehicle_announcement(self.vin)
        self._socket: Optional[socket.socket] = None
        self._stop = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._client: Optional[socket.socket] = None
        self._client_peer: Optional[str] = None
        self._client_count = 0
        self._last_rx: Optional[float] = None
        self._last_tx: Optional[float] = None
        self._rx_bytes = 0
        self._tx_bytes = 0
        self._write_lock = threading.Lock()
        self._participant = DoIPParticipant(self)
        self._request_lock = threading.Lock()
        self._active_request: Optional[DoIPRequest] = None
        self._udp_thread: Optional[threading.Thread] = None
        self._udp_socket: Optional[socket.socket] = None
        self._allowed_targets = self._resolve_allowed_targets()

    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 _resolve_allowed_targets(self) -> set[int]:
        try:
            from ..core.ecu import ECU
        except Exception:
            return set()
        targets: set[int] = set()
        for participant in self.bus.participants.values():
            if not isinstance(participant, ECU):
                continue
            for handler in participant._handlers:
                targets.add(int(handler.arbitration_id))
                targets.add(int(handler.reply_id))
        return targets

    def start(self) -> bool:
        if self._thread and self._thread.is_alive():
            return True
        self._stop.clear()
        # Bind TCP socket synchronously before starting thread
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        for attempt in range(10):
            try:
                self._socket.bind((self.host, self.port))
                break
            except OSError as e:
                self._log(f"DoIP: bind attempt {attempt + 1} failed: {e}")
                if attempt == 9:
                    return False
                time.sleep(0.1)
        self._socket.listen(1)
        self._socket.settimeout(0.5)
        self._log(f"DoIP: listening on {self.host}:{self.port}")
        print(f"DEBUG: DoIP TCP server started on {self.host}:{self.port}", flush=True)
        self.bus.attach(self._participant)
        self._start_udp_service()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        return True

    def stop(self) -> None:
        self._stop.set()
        if self._client:
            try:
                self._client.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self._client.close()
            except OSError:
                pass
            self._client = None
            self._client_peer = None
        if self._socket:
            try:
                self._socket.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self._socket.close()
            except OSError:
                pass
            self._socket = None
        if self._udp_socket:
            try:
                self._udp_socket.close()
            except OSError:
                pass
            self._udp_socket = None
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        if self._udp_thread is not None:
            self._udp_thread.join(timeout=1.0)
            self._udp_thread = None
        print("DEBUG: DoIP server stopped", flush=True)

    def handle_incoming_frame(self, frame: CANFrame) -> None:
        with self._request_lock:
            request = self._active_request
        if request is None:
            return
        if not request.is_broadcast and self._allowed_targets:
            if frame.arbitration_id not in self._allowed_targets and frame.arbitration_id != request.destination:
                return
        tx_frames = request.handle_tx_flow_control(frame)
        if tx_frames:
            self._send_can_frames(request.destination, tx_frames)
            return
        rx_frames = request.handle_rx_frame(frame)
        if rx_frames:
            self._send_can_frames(request.destination, rx_frames)

    def _run(self) -> None:
        if self._socket is None:
            return
        while not self._stop.is_set():
            try:
                client, addr = self._socket.accept()
            except OSError:
                if self._stop.is_set():
                    break
                continue
            self._client = client
            self._client_peer = f"{addr[0]}:{addr[1]}"
            self._client_count = 1
            self._log(f"DoIP: client connected from {addr}")
            self._emit_event("connection_opened", peer=self._client_peer)
            client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
            client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
            try:
                self._serve_client(client)
            except (ConnectionResetError, OSError) as exc:
                if not self._stop.is_set():
                    self._log(f"DoIP: client connection error: {exc}")
                    self._emit_event("connection_error", message=str(exc), peer=self._client_peer or "unknown")
            self._log("DoIP: client disconnected")
            self._emit_event("connection_closed", peer=self._client_peer or "unknown")
            self._client_peer = None

    def _serve_client(self, client: socket.socket) -> None:
        try:
            while True:
                message = DoIPMessage.from_socket(client)
                if message is None:
                    return
                self._last_rx = time.time()
                self._rx_bytes += len(message.bytes)
                self._emit_event("transport_rx", bytes=len(message.bytes), peer=self._client_peer or "unknown")
                if message.version != DOIP_VERSION or message.inverse_version != DOIP_INVERSE_VERSION:
                    continue
                if message.payload_type == DOIP_PAYLOAD_ROUTING_ACTIVATION_REQUEST:
                    self._handle_routing_activation(client, message.payload)
                    continue
                if message.payload_type == DOIP_PAYLOAD_DIAGNOSTIC_MESSAGE:
                    self._handle_diagnostic_message(client, message.payload)
        except ConnectionError as exc:
            if not self._stop.is_set():
                self._log(f"DoIP: client connection error: {exc}")
        finally:
            self._client = None
            self._client_count = 0
            with self._request_lock:
                self._active_request = None

    def _start_udp_service(self) -> None:
        thread = threading.Thread(target=self._udp_loop, daemon=True)
        self._udp_thread = thread
        thread.start()

    def _udp_loop(self) -> None:
        try:
            self._send_initial_announcement()
        except Exception as exc:
            self._log(f"DoIP: UDP announcement failed: {exc}")
        udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self._udp_socket = udp
        udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        udp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        try:
            udp.bind(("0.0.0.0", DOIP_PORT_UDP))
        except OSError as exc:
            self._log(f"DoIP: UDP bind failed on 0.0.0.0:{DOIP_PORT_UDP}: {exc}")
            return
        self._log(f"DoIP: UDP listening on 0.0.0.0:{DOIP_PORT_UDP}")
        print(f"DEBUG: DoIP UDP server started on 0.0.0.0:{DOIP_PORT_UDP}", flush=True)
        udp.settimeout(0.5)
        while not self._stop.is_set():
            try:
                data, addr = udp.recvfrom(256)
            except OSError:
                if self._stop.is_set():
                    break
                continue
            if not _is_vehicle_identification_request(data):
                continue
            try:
                udp.sendto(self.vehicle_announcement, addr)
            except OSError:
                continue

    def _send_initial_announcement(self) -> None:
        udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        udp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        try:
            udp.bind((self.host if self.host != "0.0.0.0" else "", 0))
        except OSError:
            pass
        broadcast = DOIP_BROADCAST_LINK_LOCAL_UDP if self.host.startswith("169.254.") else DOIP_BROADCAST_UDP
        for _ in range(3):
            try:
                udp.sendto(self.vehicle_announcement, (broadcast, DOIP_PORT_UDP))
            except OSError:
                pass
            time.sleep(0.333)
        udp.close()

    def _handle_routing_activation(self, client: socket.socket, payload: bytes) -> None:
        if len(payload) < 3:
            return
        request_source = int.from_bytes(payload[0:2], "big")
        if self.tester_address is not None and request_source != self.tester_address:
            response = _build_routing_activation_response(
                client_address=request_source,
                source_address=DOIP_ROUTING_SOURCE_ADDRESS,
                response_code=DOIP_ROUTING_INVALID_SOURCE,
            )
            self._send_doip(client, response)
            try:
                client.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                client.close()
            except OSError:
                pass
            raise ConnectionError("Invalid source address")
        response = _build_routing_activation_response(
            client_address=request_source,
            source_address=DOIP_ROUTING_SOURCE_ADDRESS,
            response_code=DOIP_ROUTING_SUCCESS,
        )
        self._send_doip(client, response)

    def _handle_diagnostic_message(self, client: socket.socket, payload: bytes) -> None:
        if len(payload) < 4:
            return
        source = int.from_bytes(payload[0:2], "big")
        target = int.from_bytes(payload[2:4], "big")
        uds_payload = list(payload[4:])
        is_broadcast = target == DOIP_BROADCAST_ADDRESS
        if not is_broadcast and self._allowed_targets and target not in self._allowed_targets:
            nack = _build_diagnostic_nack(
                source_address=target,
                target_address=source,
                nack_code=DOIP_DIAGNOSTIC_NACK_UNKNOWN_TARGET,
            )
            self._send_doip(client, nack)
            return
        ack = _build_diagnostic_ack(source_address=target, target_address=source)
        self._send_doip(client, ack)

        request = DoIPRequest(source, target, uds_payload)
        self._emit_event(
            "diagnostic_request",
            peer=self._client_peer or "unknown",
            source=source,
            destination=target,
            bytes=list(uds_payload),
        )
        with self._request_lock:
            self._active_request = request
        action = request.tx_isotp.write(uds_payload)
        if action.kind == "write_frames" and action.frames is not None:
            self._send_can_frames(request.destination, action.frames)
        timeout = 1.0
        end_time = time.time() + timeout
        while True:
            remaining = end_time - time.time()
            if remaining <= 0:
                break
            try:
                source_id, uds_bytes = request.responses.get(timeout=remaining)
            except queue.Empty:
                break
            if not uds_bytes:
                continue
            is_negative = len(uds_bytes) >= 3 and uds_bytes[0] == 0x7F
            is_response_pending = is_negative and uds_bytes[2] == 0x78
            if is_response_pending and not request.is_broadcast:
                end_time = time.time() + 5.0
            if request.is_broadcast and is_negative:
                continue
            response = _build_diagnostic_message(
                source_address=source_id,
                target_address=source,
                payload=uds_bytes,
            )
            self._send_doip(client, response)
            self._emit_event(
                "diagnostic_response",
                peer=self._client_peer or "unknown",
                source=source_id,
                destination=source,
                bytes=list(uds_bytes),
            )
            if not request.is_broadcast and not is_response_pending:
                break
        with self._request_lock:
            self._active_request = None

    def _send_can_frames(self, arbitration_id: int, frames: list[list[int]]) -> None:
        for frame_bytes in frames:
            payload = list(frame_bytes)
            dlc = len(payload)
            frame = CANFrame(arbitration_id=arbitration_id, data=payload, dlc=dlc)
            self.bus.write(frame, originator=self._participant)

    def _send_doip(self, client: socket.socket, payload: bytes) -> None:
        with self._write_lock:
            try:
                client.sendall(payload)
                self._last_tx = time.time()
                self._tx_bytes += len(payload)
                self._emit_event("transport_tx", bytes=len(payload), peer=self._client_peer or "unknown")
            except OSError:
                self._client = None

    def ui_snapshot(self) -> dict:
        return {
            "connected": self._client is not None,
            "clients": self._client_count,
            "last_rx": self._last_rx,
            "last_tx": self._last_tx,
            "rx_bytes": self._rx_bytes,
            "tx_bytes": self._tx_bytes,
        }


def _read_exact(client: socket.socket, length: int) -> bytes:
    if length <= 0:
        return b""
    buffer = bytearray()
    while len(buffer) < length:
        try:
            chunk = client.recv(length - len(buffer))
        except (ConnectionResetError, OSError):
            return b""
        if not chunk:
            return b""
        buffer.extend(chunk)
    return bytes(buffer)


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


def _build_header(payload_type: int, payload_length: int) -> bytes:
    return bytes(
        [
            DOIP_VERSION,
            DOIP_INVERSE_VERSION,
            (payload_type >> 8) & 0xFF,
            payload_type & 0xFF,
        ]
    ) + int(payload_length).to_bytes(4, "big")


def _build_vehicle_announcement(vin: str) -> bytes:
    vin = _sanitize_vin(vin)
    payload = bytearray()
    payload.extend(vin.encode("utf-8"))
    if len(payload) < 17:
        payload.extend(b"\x00" * (17 - len(payload)))
    payload.extend([0x00, 0x01])
    payload.extend([0x00] * 6)
    payload.extend([0x00] * 6)
    payload.extend([0x00])
    header = _build_header(DOIP_PAYLOAD_VEHICLE_IDENT_RESPONSE, len(payload))
    return header + bytes(payload)


def _build_routing_activation_response(client_address: int, source_address: int, response_code: int) -> bytes:
    payload = bytearray()
    payload.extend(int(client_address).to_bytes(2, "big"))
    payload.extend(int(source_address).to_bytes(2, "big"))
    payload.append(int(response_code) & 0xFF)
    payload.extend([0x00, 0x00, 0x00, 0x00])
    header = _build_header(DOIP_PAYLOAD_ROUTING_ACTIVATION_RESPONSE, len(payload))
    return header + bytes(payload)


def _build_diagnostic_ack(source_address: int, target_address: int) -> bytes:
    payload = bytearray()
    payload.extend(int(source_address).to_bytes(2, "big"))
    payload.extend(int(target_address).to_bytes(2, "big"))
    payload.append(0x00)
    header = _build_header(DOIP_PAYLOAD_DIAGNOSTIC_ACK, len(payload))
    return header + bytes(payload)


def _build_diagnostic_nack(source_address: int, target_address: int, nack_code: int) -> bytes:
    payload = bytearray()
    payload.extend(int(source_address).to_bytes(2, "big"))
    payload.extend(int(target_address).to_bytes(2, "big"))
    payload.append(int(nack_code) & 0xFF)
    header = _build_header(DOIP_PAYLOAD_DIAGNOSTIC_NACK, len(payload))
    return header + bytes(payload)


def _build_diagnostic_message(source_address: int, target_address: int, payload: list[int]) -> bytes:
    diag_payload = bytearray()
    diag_payload.extend(int(source_address).to_bytes(2, "big"))
    diag_payload.extend(int(target_address).to_bytes(2, "big"))
    diag_payload.extend(payload)
    header = _build_header(DOIP_PAYLOAD_DIAGNOSTIC_MESSAGE, len(diag_payload))
    return header + bytes(diag_payload)


def _is_vehicle_identification_request(data: bytes) -> bool:
    if len(data) < 8:
        return False
    if data[0] != DOIP_VERSION or data[1] != DOIP_INVERSE_VERSION:
        return False
    payload_type = (data[2] << 8) | data[3]
    return payload_type in (
        DOIP_PAYLOAD_VEHICLE_IDENT_REQUEST,
        DOIP_PAYLOAD_VEHICLE_IDENT_REQUEST_EID,
        DOIP_PAYLOAD_VEHICLE_IDENT_REQUEST_VIN,
    )
