from __future__ import annotations

from dataclasses import dataclass
from typing import List

from .uds import ISOTP


class ISOTPError(Exception):
    pass


class MessageTooSmall(ISOTPError):
    pass


class MessageTooBig(ISOTPError):
    pass


class ProtocolViolation(ISOTPError):
    pass


class FlowStatus:
    clear_to_send = 0x30
    wait = 0x31
    overflow = 0x32


class FrameType:
    single = 0x00
    first = 0x01
    consecutive = 0x02


FD_CAN_DLC_LENGTHS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64)
FD_STANDARD_FRAME_LENGTH = 64
FD_EXTENDED_FRAME_LENGTH = 63


def is_valid_can_fd_length(length: int) -> bool:
    return int(length) in FD_CAN_DLC_LENGTHS


def next_valid_can_fd_length(length: int) -> int:
    length = max(int(length), 0)
    for candidate in FD_CAN_DLC_LENGTHS:
        if candidate >= length:
            return candidate
    return FD_CAN_DLC_LENGTHS[-1]


def is_valid_fd_frame_length(length: int, *, extended_addressing: bool) -> bool:
    length = int(length)
    if extended_addressing:
        if not (0 <= length <= FD_EXTENDED_FRAME_LENGTH):
            return False
        return is_valid_can_fd_length(length + 1)
    if not (0 <= length <= FD_STANDARD_FRAME_LENGTH):
        return False
    return is_valid_can_fd_length(length)


def next_valid_fd_frame_length(required_length: int, *, extended_addressing: bool) -> int:
    required_length = max(int(required_length), 0)
    if extended_addressing:
        physical = next_valid_can_fd_length(required_length + 1)
        return max(physical - 1, 0)
    return next_valid_can_fd_length(required_length)


def single_frame_payload_capacity_fd(frame_length: int) -> int:
    frame_length = max(int(frame_length), 0)
    return frame_length - 2 if frame_length > ISOTP.frame_length else frame_length - 1


@dataclass
class FlowControlFrame:
    flow_status: int
    block_size: int
    separation_time: int

    @property
    def bytes(self) -> List[int]:
        return [self.flow_status, self.block_size, self.separation_time]

    @classmethod
    def from_bytes(cls, payload: List[int]) -> "FlowControlFrame | None":
        if len(payload) < 3:
            return None
        flow_status = payload[0]
        if flow_status not in (FlowStatus.clear_to_send, FlowStatus.wait, FlowStatus.overflow):
            return None
        return cls(flow_status=flow_status, block_size=payload[1], separation_time=payload[2])


@dataclass
class ISOTPAction:
    kind: str
    bytes: List[int] | None = None
    frames: List[List[int]] | None = None
    separation_time: int = 0
    is_last_batch: bool = True

    @staticmethod
    def process(bytes_: List[int]) -> "ISOTPAction":
        return ISOTPAction(kind="process", bytes=bytes_)

    @staticmethod
    def write_frames(frames: List[List[int]], separation_time: int, is_last_batch: bool) -> "ISOTPAction":
        return ISOTPAction(kind="write_frames", frames=frames, separation_time=separation_time, is_last_batch=is_last_batch)

    @staticmethod
    def wait() -> "ISOTPAction":
        return ISOTPAction(kind="wait")


class ISOTPTransceiver:
    def __init__(
        self,
        behavior: str = "defensive",
        block_size: int = 0x20,
        separation_time_ms: int = 0x00,
        frame_length: int = ISOTP.frame_length,
    ) -> None:
        self.behavior = behavior
        self.flow_control_frame = FlowControlFrame(
            flow_status=FlowStatus.clear_to_send,
            block_size=block_size,
            separation_time=separation_time_ms,
        )
        self.frame_length = frame_length
        self.state = "idle"

        self._writing_payload: List[int] = []
        self._sequence_number = 0x00

        self._reading_payload: List[int] = []
        self._remaining_payload = 0
        self._flow_control_counter = 0

    def _pad_frame(self, payload: List[int]) -> List[int]:
        width = max(int(self.frame_length), 0)
        if width <= 0:
            return list(payload)
        if len(payload) >= width:
            return list(payload[:width])
        return list(payload) + [0x00] * (width - len(payload))

    def write(self, payload: List[int]) -> ISOTPAction:
        if len(payload) > ISOTP.maximum_frame_size:
            raise MessageTooBig("ISOTP payload too long")
        single_capacity = max(self.frame_length - 1, 0)
        if len(payload) <= single_capacity:
            frame = self._pad_frame([len(payload)] + payload)
            return ISOTPAction.write_frames([frame], separation_time=0, is_last_batch=True)
        pci_hi = (FrameType.first << 4) | (len(payload) >> 8)
        pci_lo = len(payload) & 0xFF
        first_capacity = max(self.frame_length - 2, 0)
        first_payload = payload[:first_capacity]
        self._writing_payload = payload[first_capacity:]
        self._sequence_number = 0x01
        self.state = "sending"
        frame = self._pad_frame([pci_hi, pci_lo] + first_payload)
        return ISOTPAction.write_frames([frame], separation_time=0, is_last_batch=False)

    def did_read(self, payload: List[int]) -> ISOTPAction:
        if len(payload) != self.frame_length:
            raise ProtocolViolation(f"Invalid ISOTP frame length: {len(payload)}, expected {self.frame_length}")
        if self.behavior != "defensive":
            if self.state == "sending":
                return self._parse_flow_control(payload)
            return self._parse_data_frame(payload)
        try:
            if self.state == "sending":
                return self._parse_flow_control(payload)
            return self._parse_data_frame(payload)
        except ProtocolViolation:
            self._reset()
            try:
                return self._parse_data_frame(payload)
            except Exception:
                self._reset()
                return ISOTPAction.wait()

    def _reset(self) -> None:
        self.state = "idle"
        self._writing_payload = []
        self._sequence_number = 0
        self._reading_payload = []
        self._remaining_payload = 0
        self._flow_control_counter = 0

    def _parse_flow_control(self, payload: List[int]) -> ISOTPAction:
        frame = FlowControlFrame.from_bytes(payload)
        if frame is None:
            self._reset()
            raise ProtocolViolation("Did not receive flow control while sending")
        if frame.flow_status == FlowStatus.wait:
            return ISOTPAction.wait()
        if frame.flow_status == FlowStatus.overflow:
            self._reset()
            return ISOTPAction.wait()
        number_of_frames = int(frame.block_size) if frame.block_size != 0 else 2**31 - 1
        consecutive_capacity = max(self.frame_length - 1, 0)
        frames: List[List[int]] = []
        for _ in range(number_of_frames):
            if consecutive_capacity <= 0:
                break
            pci = (FrameType.consecutive << 4) | (self._sequence_number & 0x0F)
            length = min(consecutive_capacity, len(self._writing_payload))
            payload_slice = self._writing_payload[:length]
            self._writing_payload = self._writing_payload[length:]
            frames.append(self._pad_frame([pci] + payload_slice))
            if not self._writing_payload:
                break
            self._sequence_number = (self._sequence_number + 1) % 16
        if not self._writing_payload:
            self._reset()
        return ISOTPAction.write_frames(frames, separation_time=frame.separation_time, is_last_batch=not self._writing_payload)

    def _parse_data_frame(self, payload: List[int]) -> ISOTPAction:
        if payload[0] == 0x00:
            raise ProtocolViolation("Invalid ISOTP PCI")
        frame_type = payload[0] >> 4
        if frame_type == FrameType.single:
            if self.state != "idle":
                raise ProtocolViolation("Received SF while not idle")
            dl = payload[0] & 0x0F
            single_capacity = max(self.frame_length - 1, 0)
            if dl > single_capacity:
                raise ProtocolViolation("Invalid SF length")
            payload_end = 1 + dl
            return ISOTPAction.process(payload[1:payload_end])
        if frame_type == FrameType.first:
            if self.state != "idle":
                raise ProtocolViolation("Received FF while not idle")
            pci_hi = payload[0] & 0x0F
            pci_lo = payload[1]
            pci = (pci_hi << 8) | pci_lo
            single_capacity = max(self.frame_length - 1, 0)
            if pci <= single_capacity:
                raise ProtocolViolation("Invalid FF length")
            first_capacity = max(self.frame_length - 2, 0)
            self._remaining_payload = pci - first_capacity
            self._reading_payload = list(payload[2:])
            self._flow_control_counter = self.flow_control_frame.block_size
            self.state = "receiving"
            self._sequence_number = 1
            fc = self._pad_frame(self.flow_control_frame.bytes)
            return ISOTPAction.write_frames([fc], separation_time=0, is_last_batch=False)
        if frame_type == FrameType.consecutive:
            if self.state != "receiving":
                raise ProtocolViolation("Received CF while not receiving")
            sn = payload[0] & 0x0F
            if sn != self._sequence_number:
                raise ProtocolViolation("Wrong sequence number")
            self._sequence_number = (self._sequence_number + 1) % 16
            payload_length = min(max(self.frame_length - 1, 0), self._remaining_payload)
            self._reading_payload.extend(payload[1:1 + payload_length])
            self._remaining_payload -= payload_length
            if self._remaining_payload <= 0:
                message = list(self._reading_payload)
                self._reset()
                return ISOTPAction.process(message)
            if self.flow_control_frame.block_size == 0:
                return ISOTPAction.wait()
            self._flow_control_counter -= 1
            if self._flow_control_counter == 0:
                self._flow_control_counter = self.flow_control_frame.block_size
                fc = self._pad_frame(self.flow_control_frame.bytes)
                return ISOTPAction.write_frames([fc], separation_time=0, is_last_batch=False)
            return ISOTPAction.wait()
        raise ProtocolViolation("Unknown frame type")


class ISOTPFDTransceiver:
    def __init__(
        self,
        behavior: str = "defensive",
        block_size: int = 0x20,
        separation_time_ms: int = 0x00,
        frame_length: int = FD_STANDARD_FRAME_LENGTH,
        extended_addressing: bool = False,
    ) -> None:
        self.behavior = behavior
        self.extended_addressing = bool(extended_addressing)
        self.flow_control_frame = FlowControlFrame(
            flow_status=FlowStatus.clear_to_send,
            block_size=block_size,
            separation_time=separation_time_ms,
        )
        self.frame_length = self._resolve_max_frame_length(frame_length)
        self.state = "idle"

        self._writing_payload: List[int] = []
        self._sequence_number = 0x00

        self._reading_payload: List[int] = []
        self._remaining_payload = 0
        self._flow_control_counter = 0

    def configure_link(self, frame_length: int, extended_addressing: bool) -> None:
        self.extended_addressing = bool(extended_addressing)
        self.frame_length = self._resolve_max_frame_length(frame_length)

    def _resolve_max_frame_length(self, requested: int) -> int:
        minimum = ISOTP.frame_length - 1 if self.extended_addressing else ISOTP.frame_length
        maximum = FD_EXTENDED_FRAME_LENGTH if self.extended_addressing else FD_STANDARD_FRAME_LENGTH
        if requested <= 0:
            return maximum
        clamped = max(minimum, min(int(requested), maximum))
        if is_valid_fd_frame_length(clamped, extended_addressing=self.extended_addressing):
            return clamped
        return min(
            next_valid_fd_frame_length(clamped, extended_addressing=self.extended_addressing),
            maximum,
        )

    def _dynamic_frame_length(self, required: int) -> int:
        candidate = next_valid_fd_frame_length(required, extended_addressing=self.extended_addressing)
        return min(candidate, self.frame_length)

    def _pad_frame(self, payload: List[int], width: int) -> List[int]:
        if len(payload) >= width:
            return list(payload[:width])
        return list(payload) + [0x00] * (width - len(payload))

    def _build_single_frame(self, payload: List[int]) -> List[int]:
        if len(payload) <= 0x07:
            frame = [len(payload)] + payload
            width = self._dynamic_frame_length(len(payload) + 1)
            return self._pad_frame(frame, width)
        frame = [0x00, len(payload) & 0xFF] + payload
        width = self._dynamic_frame_length(len(payload) + 2)
        return self._pad_frame(frame, width)

    def _build_first_frame(self, payload_length: int, payload: List[int]) -> List[int]:
        pci_hi = (FrameType.first << 4) | (payload_length >> 8)
        pci_lo = payload_length & 0xFF
        frame = [pci_hi, pci_lo] + payload
        width = self._dynamic_frame_length(len(frame))
        return self._pad_frame(frame, width)

    def _build_consecutive_frame(self, payload: List[int]) -> List[int]:
        pci = (FrameType.consecutive << 4) | (self._sequence_number & 0x0F)
        frame = [pci] + payload
        width = self._dynamic_frame_length(len(frame))
        return self._pad_frame(frame, width)

    def _build_flow_control_frame(self) -> List[int]:
        frame = self.flow_control_frame.bytes
        width = self._dynamic_frame_length(len(frame))
        return self._pad_frame(frame, width)

    def _reset(self) -> None:
        self.state = "idle"
        self._writing_payload = []
        self._sequence_number = 0
        self._reading_payload = []
        self._remaining_payload = 0
        self._flow_control_counter = 0

    def write(self, payload: List[int]) -> ISOTPAction:
        if len(payload) > ISOTP.maximum_frame_size:
            raise MessageTooBig("ISOTP payload too long")
        single_capacity = max(single_frame_payload_capacity_fd(self.frame_length), 0)
        if len(payload) <= single_capacity:
            frame = self._build_single_frame(payload)
            return ISOTPAction.write_frames([frame], separation_time=0, is_last_batch=True)

        first_capacity = max(self.frame_length - 2, 0)
        first_payload = payload[:first_capacity]
        self._writing_payload = payload[first_capacity:]
        self._sequence_number = 0x01
        self.state = "sending"
        frame = self._build_first_frame(len(payload), first_payload)
        return ISOTPAction.write_frames([frame], separation_time=0, is_last_batch=False)

    def did_read(self, payload: List[int]) -> ISOTPAction:
        if not payload:
            raise ProtocolViolation("Invalid ISOTP frame length: 0")
        if len(payload) > self.frame_length:
            raise ProtocolViolation(
                f"Invalid ISOTP/FD frame length: {len(payload)}, max {self.frame_length}"
            )
        if not is_valid_fd_frame_length(len(payload), extended_addressing=self.extended_addressing):
            raise ProtocolViolation(f"Invalid CAN-FD DLC length: {len(payload)}")
        if self.behavior != "defensive":
            if self.state == "sending":
                return self._parse_flow_control(payload)
            return self._parse_data_frame(payload)
        try:
            if self.state == "sending":
                return self._parse_flow_control(payload)
            return self._parse_data_frame(payload)
        except ProtocolViolation:
            self._reset()
            try:
                return self._parse_data_frame(payload)
            except Exception:
                self._reset()
                return ISOTPAction.wait()

    def _parse_flow_control(self, payload: List[int]) -> ISOTPAction:
        if len(payload) < 3:
            self._reset()
            raise ProtocolViolation("Did not receive flow control while sending")
        frame = FlowControlFrame.from_bytes(payload[:3])
        if frame is None:
            self._reset()
            raise ProtocolViolation("Did not receive flow control while sending")
        if frame.flow_status == FlowStatus.wait:
            return ISOTPAction.wait()
        if frame.flow_status == FlowStatus.overflow:
            self._reset()
            return ISOTPAction.wait()

        number_of_frames = int(frame.block_size) if frame.block_size != 0 else 2**31 - 1
        consecutive_capacity = max(self.frame_length - 1, 0)
        frames: List[List[int]] = []
        for _ in range(number_of_frames):
            if consecutive_capacity <= 0:
                break
            length = min(consecutive_capacity, len(self._writing_payload))
            if length <= 0:
                break
            payload_slice = self._writing_payload[:length]
            self._writing_payload = self._writing_payload[length:]
            frames.append(self._build_consecutive_frame(payload_slice))
            if not self._writing_payload:
                break
            self._sequence_number = (self._sequence_number + 1) % 16
        if not self._writing_payload:
            self._reset()
        return ISOTPAction.write_frames(
            frames,
            separation_time=frame.separation_time,
            is_last_batch=not self._writing_payload,
        )

    def _parse_data_frame(self, payload: List[int]) -> ISOTPAction:
        if payload[0] == 0x00 and len(payload) <= ISOTP.frame_length:
            raise ProtocolViolation("Invalid ISOTP PCI")

        frame_type = payload[0] >> 4
        if frame_type == FrameType.single:
            if self.state != "idle":
                raise ProtocolViolation("Received SF while not idle")
            header_size = 1
            dl = payload[0] & 0x0F
            if len(payload) > ISOTP.frame_length:
                if dl != 0 or len(payload) < 2:
                    raise ProtocolViolation("Invalid SF length")
                header_size = 2
                dl = payload[1]
            if dl == 0:
                raise ProtocolViolation("Invalid SF length")
            single_capacity = max(single_frame_payload_capacity_fd(len(payload)), 0)
            if dl > single_capacity:
                raise ProtocolViolation("Invalid SF length")
            if dl > len(payload) - header_size:
                raise ProtocolViolation("Invalid SF length")
            return ISOTPAction.process(payload[header_size:header_size + dl])

        if frame_type == FrameType.first:
            if self.state != "idle":
                raise ProtocolViolation("Received FF while not idle")
            if len(payload) < 3:
                raise ProtocolViolation("Invalid FF length")
            pci_hi = payload[0] & 0x0F
            pci_lo = payload[1]
            pci = (pci_hi << 8) | pci_lo
            first_capacity = max(len(payload) - 2, 0)
            if pci <= first_capacity:
                raise ProtocolViolation("Invalid FF length")
            self._remaining_payload = pci - first_capacity
            self._reading_payload = list(payload[2:])
            self._flow_control_counter = self.flow_control_frame.block_size
            self.state = "receiving"
            self._sequence_number = 1
            fc = self._build_flow_control_frame()
            return ISOTPAction.write_frames([fc], separation_time=0, is_last_batch=False)

        if frame_type == FrameType.consecutive:
            if self.state != "receiving":
                raise ProtocolViolation("Received CF while not receiving")
            if len(payload) < 2:
                raise ProtocolViolation("Invalid CF length")
            sn = payload[0] & 0x0F
            if sn != self._sequence_number:
                raise ProtocolViolation("Wrong sequence number")
            self._sequence_number = (self._sequence_number + 1) % 16
            payload_length = min(max(len(payload) - 1, 0), self._remaining_payload)
            self._reading_payload.extend(payload[1:1 + payload_length])
            self._remaining_payload -= payload_length
            if self._remaining_payload <= 0:
                message = list(self._reading_payload)
                self._reset()
                return ISOTPAction.process(message)
            if self.flow_control_frame.block_size == 0:
                return ISOTPAction.wait()
            self._flow_control_counter -= 1
            if self._flow_control_counter == 0:
                self._flow_control_counter = self.flow_control_frame.block_size
                fc = self._build_flow_control_frame()
                return ISOTPAction.write_frames([fc], separation_time=0, is_last_batch=False)
            return ISOTPAction.wait()

        raise ProtocolViolation("Unknown frame type")
