from __future__ import annotations

import threading
import time
from dataclasses import dataclass
from typing import List, Optional

from pymulator.core.bus import CANBusEmulation, CANBusParticipant
from pymulator.core.can import CANFrame
from pymulator.core.isotp import FrameType

from ..errors import AdapterError
from ..protocols import AUTO, BusProtocol, CAN_11B_250K, CAN_11B_500K, CAN_29B_250K, CAN_29B_500K, USER1_11B_125K, USER2_11B_50K
from .periodic_message import PeriodicMessage
from .request import Request
from .request_handler import RequestHandler


class Output:
    bus_error = "BUS ERROR"
    bus_init_error = "BUS INIT: ERROR"
    carriage_return = "\r"
    invalid_command = "?"
    linefeed = "\n"
    no_data = "NO DATA"
    ok = "OK"
    prompt = ">"
    searching = "SEARCHING..."
    stopped = "STOPPED"
    unable_to_connect = "UNABLE TO CONNECT"
    empty = ""


class AdapterOutputWriter:
    def write(self, output: str) -> None:
        raise NotImplementedError


class Adapter(CANBusParticipant):
    valid_character_set = set(".\r#@,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
    connection_command = "0100"

    def __init__(self, name: str) -> None:
        super().__init__()
        self.name = name
        self.output: AdapterOutputWriter
        self.command_sets: List["CommandSetHandler"] = []
        self.bus: Optional[CANBusEmulation] = None
        self._lock = threading.Lock()
        self.active_request: Optional[Request] = None
        self._is_connecting = False
        self._last_request: Optional[Request] = None
        self._request_handler = RequestHandler()

        self.lock_status = None
        self.elm_at1_descriptor = "OBDII to RS232 Interpreter"
        self.elm_at2_name = "ELMpythonator"
        self.elm_ati_version = "ELM327 1.4a"
        self.elm_atign_state = "ON"
        self.elm_atrv_voltage = "12.1V"
        self.stn_sti_version = "STN2200 2.2.0"
        self.stn_stix_version = "STN2200 2.2.0 [26.12.1971]"
        self.stn_stsn_serial = "123456789012"
        self.stn_stmfr_manufacturer = "OBD Solutions LLC"

        self.adaptive_timing = True
        self.echo = True
        self.linefeed = False
        self.spaces = True
        self.can_auto_format = True
        self.variable_data_length = False
        self.can_filler_byte = 0x55
        self.print_headers = False
        self.responses_enabled = True
        self.request_header = 0
        self.request_header_extension: Optional[int] = None
        self.reply_header_extension: Optional[int] = None
        self.can_reply_filter = 0
        self.can_reply_mask = 0
        self.bus_timeout = 200
        self.nrc_busy_bus_timeout = 5000
        self.auto_segmentation_tx = False
        self.auto_segmentation_rx = False
        self.bitrate = -1
        self.monitor_mode_active = False
        self.next_bus_command_modifiers: List[BusCommandModifier] = []
        self._selected_protocol = AUTO
        self.auto_protocol_start: Optional[BusProtocol] = None
        self._connected_header: Optional[int] = None
        self.periodic_messages: List[PeriodicMessage] = []

    @property
    def selected_protocol(self) -> BusProtocol:
        return self._selected_protocol

    @selected_protocol.setter
    def selected_protocol(self, proto: BusProtocol) -> None:
        self._selected_protocol = proto
        next_bitrate = proto.bitrate
        self.reset_can_reply_arbitration()
        if next_bitrate > 0 and next_bitrate != self.bitrate:
            self.bitrate = next_bitrate

    @property
    def connected(self) -> Optional[int]:
        return self._connected_header

    @connected.setter
    def connected(self, header: Optional[int]) -> None:
        self._connected_header = header
        if header is None:
            return
        if self.selected_protocol == AUTO:
            self.selected_protocol = self._infer_protocol(header)

    @property
    def current_request_header(self) -> int:
        if self.request_header:
            return self.request_header
        if self.selected_protocol in (CAN_11B_250K, CAN_11B_500K, USER1_11B_125K, USER2_11B_50K):
            return 0x7DF
        if self.selected_protocol in (CAN_29B_250K, CAN_29B_500K):
            return 0x18DB33F1
        return 0

    @property
    def expects_extended_addressing_on_receive(self) -> bool:
        return self.request_header_extension is not None or self.reply_header_extension is not None

    @property
    def isotp_frame_length(self) -> int:
        return 7 if self.expects_extended_addressing_on_receive else 8

    def reset(self) -> None:
        self.echo = True
        self.spaces = True
        self.print_headers = False
        self.linefeed = False
        self.can_auto_format = True
        self.variable_data_length = False
        self.can_filler_byte = 0x55
        self.selected_protocol = AUTO
        self.auto_protocol_start = None
        self.request_header = 0
        self.request_header_extension = None
        self.reply_header_extension = None
        self.can_reply_filter = 0
        self.can_reply_mask = 0
        self.responses_enabled = True
        self.clear_periodic_messages()

    def install_command_set(self, command_set: "CommandSetHandler") -> None:
        self.command_sets.append(command_set)

    def feed_command(self, command: str) -> None:
        try:
            if self.echo and ("\r" in command or "\n" in command):
                self.output.write(command)
            stripped = command.strip().upper().replace(" ", "")
            if self.monitor_mode_active:
                self.monitor_mode_active = False
                self.output.write(Output.stopped)
                self.write_line_ending()
                self.write_prompt()
                return
            if len(stripped) == 1 and stripped != ".":
                raise AdapterError.invalid_command()
            if not stripped:
                request = self._last_request
            else:
                request = Request.from_string(stripped)
            if request is None:
                raise AdapterError.invalid_command()
            if request.is_bus_command and self.next_bus_command_modifiers:
                expected = None
                timeout_override = None
                for modifier in self.next_bus_command_modifiers:
                    if modifier.kind == "expected_responses":
                        expected = modifier.value
                    elif modifier.kind == "timeout":
                        timeout_override = int(modifier.value * 1000.0)
                request.override_expected_answers(expected)
                request.override_timeout(timeout_override)
                self.next_bus_command_modifiers = []
            if request.is_bus_command and not self._is_connecting and self.selected_protocol == AUTO and self.connected is None:
                self.connect()
            self._last_request = request
            output_stream = self._request_handler.feed(request, self)
            for output in output_stream:
                if self.responses_enabled:
                    self.output.write(output)
                    self.write_line_ending()
            self.write_prompt()
        except AdapterError as error:
            if error.kind == "start_monitor_mode":
                self.monitor_mode_active = True
                return
            output = self._map_error_to_output(error)
            self.output.write(output)
            self.write_line_ending()
            self.write_prompt()
        except Exception as exc:
            raise RuntimeError(f"Unhandled error: {exc}") from exc

    def _map_error_to_output(self, error: AdapterError) -> str:
        if error.kind == "invalid_command":
            return Output.invalid_command
        if error.kind == "no_data":
            return Output.no_data
        if error.kind == "bus_init_error":
            return Output.bus_init_error
        if error.kind == "unable_to_connect":
            return Output.unable_to_connect
        return Output.empty

    def format_received_bytes(self, arbitration_id: int, bytes_: List[int]) -> str:
        bytes_to_print = list(bytes_)
        formatted_data_length: Optional[int] = None
        if self.can_auto_format and not self.auto_segmentation_rx and bytes_to_print:
            frame_type = bytes_to_print[0] >> 4
            if frame_type == FrameType.single:
                declared = bytes_to_print[0] & 0x0F
                payload = bytes_to_print[1:1 + declared]
                bytes_to_print = payload
                if self.print_headers:
                    # With ATH1 + CAF1, ELM/STN print the post-processed payload length
                    # byte between header and payload for single-frame CAN responses.
                    formatted_data_length = len(payload)
        parts: List[str] = []
        if self.print_headers:
            parts.append(self._format_header(arbitration_id))
            if formatted_data_length is not None:
                parts.append(f"{formatted_data_length:02X}")
        parts.extend(f"{byte:02X}" for byte in bytes_to_print)
        separator = " " if self.spaces else ""
        output = separator.join(parts)
        if self._is_connecting:
            return ""
        return output

    def write_line_ending(self) -> None:
        self.output.write(Output.carriage_return)
        if self.linefeed:
            self.output.write(Output.linefeed)

    def write_prompt(self) -> None:
        self.write_line_ending()
        self.output.write(Output.prompt)

    def connect(self) -> None:
        if not self.bus:
            raise AdapterError.unable_to_connect()
        self._is_connecting = True
        self.output.write(Output.searching)
        self.write_line_ending()
        request = Request.from_string(self.connection_command)
        protocols_to_try = [CAN_11B_500K, CAN_29B_500K, CAN_11B_250K, CAN_29B_250K]
        if self.auto_protocol_start in protocols_to_try:
            protocols_to_try.remove(self.auto_protocol_start)
            protocols_to_try.insert(0, self.auto_protocol_start)
        for proto in protocols_to_try:
            self.selected_protocol = proto
            try:
                self.bus.operate(self.bitrate)
                time.sleep(0.1)
                response_stream = self._request_handler.feed(request, self)
                for _ in response_stream:
                    pass
            except Exception:
                continue
            if self.connected is not None:
                break
        self._is_connecting = False
        if self.connected is None:
            raise AdapterError.unable_to_connect()

    def bytes_by_applying_request_extension(self, bytes_: List[int]) -> List[int]:
        if self.request_header_extension is None:
            return bytes_
        payload = [self.request_header_extension] + list(bytes_)
        if len(payload) > 8:
            raise AdapterError.invalid_command()
        return payload

    def frame_bytes_for_transmit(self, bytes_: List[int]) -> List[int]:
        payload = self.bytes_by_applying_request_extension(bytes_)
        if self.variable_data_length:
            return payload
        if len(payload) < 8:
            payload = payload + [self.can_filler_byte] * (8 - len(payload))
        return payload

    def stripped_response_bytes(self, bytes_: List[int]) -> Optional[List[int]]:
        if not self.expects_extended_addressing_on_receive:
            return list(bytes_)
        if not bytes_:
            return None
        extension_byte = bytes_[0]
        if self.reply_header_extension is not None and extension_byte != self.reply_header_extension:
            return None
        return list(bytes_[1:])

    def reset_can_reply_arbitration(self) -> None:
        if self.selected_protocol in (CAN_11B_500K, CAN_11B_250K, USER1_11B_125K, USER2_11B_50K):
            self.can_reply_mask = 0x7F8
            self.can_reply_filter = 0x7E8
        elif self.selected_protocol in (CAN_29B_500K, CAN_29B_250K):
            self.can_reply_mask = 0x1FFFFF00
            self.can_reply_filter = 0x18DAF100

    def _infer_protocol(self, header: int) -> BusProtocol:
        if header <= 0x7FF and self.bitrate == 250000:
            return CAN_11B_250K
        if header <= 0x7FF and self.bitrate == 500000:
            return CAN_11B_500K
        if header > 0x7FF and self.bitrate == 250000:
            return CAN_29B_250K
        if header > 0x7FF and self.bitrate == 500000:
            return CAN_29B_500K
        return AUTO

    def _format_header(self, header: int) -> str:
        width = 8 if header > 0x7FF else 3
        return f"{header:0{width}X}"

    def _request_for_frame(self, frame: CANFrame) -> Optional[Request]:
        if not self.active_request or not self.active_request.is_bus_command:
            return None
        if not self._is_arbitration_passing(frame.arbitration_id):
            return None
        return self.active_request

    def _is_arbitration_passing(self, arbitration_id: int) -> bool:
        if self.can_reply_mask == 0 and self.can_reply_filter == 0:
            return True
        return (arbitration_id & self.can_reply_mask) == (self.can_reply_filter & self.can_reply_mask)

    def add_periodic_message(self, periodic_message: PeriodicMessage) -> None:
        stop_event = threading.Event()
        periodic_message.timer = stop_event

        def _loop() -> None:
            interval = periodic_message.interval_ms / 1000.0
            while not stop_event.is_set():
                if self.bus and len(periodic_message.bytes) < 8:
                    base_bytes = [len(periodic_message.bytes)] + periodic_message.bytes
                    try:
                        frame_bytes = self.frame_bytes_for_transmit(base_bytes)
                    except AdapterError:
                        break
                    frame = CANFrame(arbitration_id=periodic_message.header, data=frame_bytes)
                    try:
                        self.bus.write(frame, originator=self)
                    except Exception:
                        pass
                stop_event.wait(interval)

        thread = threading.Thread(target=_loop, daemon=True)
        thread.start()
        self.periodic_messages.append(periodic_message)

    def clear_periodic_message(self, message_id: int) -> None:
        for idx, message in enumerate(self.periodic_messages):
            if message.id == message_id:
                if message.timer:
                    message.timer.set()
                self.periodic_messages.pop(idx)
                return

    def clear_periodic_messages(self) -> None:
        for message in self.periodic_messages:
            if message.timer:
                message.timer.set()
        self.periodic_messages = []

    def register(self, bus: CANBusEmulation) -> None:
        self.bus = bus

    def unregister(self, bus: CANBusEmulation) -> None:
        self.bus = None

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        if self.connected is None:
            self.connected = frame.arbitration_id
        if self.monitor_mode_active:
            payload = self.stripped_response_bytes(frame.data)
            if payload is None:
                return
            formatted = self.format_received_bytes(frame.arbitration_id, payload)
            self.output.write(formatted)
            self.write_line_ending()
            return
        active_request = self._request_for_frame(frame)
        if not active_request:
            return
        handler = active_request.bus_command_handler
        if handler:
            handler.continue_transmission(frame, active_request)


@dataclass
class BusCommandModifier:
    kind: str
    value: int | float
