from __future__ import annotations

import random
import threading
import time
import uuid
from typing import Any, Callable, List, Optional

from .actions import LockStrategy
from .addressing import Addressing
from .automotive import UdsNegativeResponse
from .can import CANFrame
from .isotp import (
    FD_EXTENDED_FRAME_LENGTH,
    FD_STANDARD_FRAME_LENGTH,
    FlowStatus,
    ISOTPAction,
    ISOTPFDTransceiver,
    ISOTPTransceiver,
    next_valid_fd_frame_length,
    single_frame_payload_capacity_fd,
)
from .message import Message
from .uds import NegativeResponseCode, ISOTP
from .sessions.base import CommandHandler
from .bus import CANBusEmulation, CANBusParticipant
from .diagnostic_interpreter import interpret_diagnostic_message


class ECU(CANBusParticipant):
    def __init__(self, name: str, handlers: List[CommandHandler]) -> None:
        super().__init__()
        self.name = name
        self._handlers = list(handlers)
        self._bus: Optional[CANBusEmulation] = None
        self._transport_profile = "isotp"
        self._isotp_block_size = 0x20
        self._isotp_separation_time = 0
        self._isotp = self._create_isotp_transceiver()
        self._queue = threading.Lock()
        self.enable_logging = False
        self.diagnostic_log_sink: Optional[Callable[[dict[str, Any]], None]] = None
        self._fc_wait_count = 0
        self._fc_wait_ms = 50.0
        self._fc_overflow = False
        self._fc_wait_probability = 0.0
        self._fc_overflow_probability = 0.0
        self._fc_rng = random.Random()

    @property
    def transport_profile(self) -> str:
        return self._transport_profile

    def _normalize_transport_profile(self, value: str | None) -> str:
        if str(value or "").strip().lower() == "isotp_fd":
            return "isotp_fd"
        return "isotp"

    def set_transport_profile(self, value: str | None) -> None:
        normalized = self._normalize_transport_profile(value)
        if normalized == self._transport_profile:
            return
        self._transport_profile = normalized
        self._isotp = self._create_isotp_transceiver()

    def _create_isotp_transceiver(self) -> ISOTPTransceiver | ISOTPFDTransceiver:
        if self._transport_profile == "isotp_fd":
            return ISOTPFDTransceiver(
                block_size=self._isotp_block_size,
                separation_time_ms=self._isotp_separation_time,
                frame_length=FD_STANDARD_FRAME_LENGTH,
            )
        return ISOTPTransceiver(
            block_size=self._isotp_block_size,
            separation_time_ms=self._isotp_separation_time,
        )

    def _refresh_isotp_parameters_from_handler(self, handler: CommandHandler) -> None:
        if hasattr(handler, "isotp_block_size"):
            self._isotp_block_size = getattr(handler, "isotp_block_size", 0x20)
            self._isotp_separation_time = getattr(handler, "isotp_separation_time", 0)
        self._isotp = self._create_isotp_transceiver()

    def _effective_frame_length(self, uses_extended_addressing: bool) -> int:
        if self._transport_profile == "isotp_fd":
            return FD_EXTENDED_FRAME_LENGTH if uses_extended_addressing else FD_STANDARD_FRAME_LENGTH
        return ISOTP.frame_length - 1 if uses_extended_addressing else ISOTP.frame_length

    def _configure_isotp_link(self, uses_extended_addressing: bool) -> None:
        frame_length = self._effective_frame_length(uses_extended_addressing)
        if isinstance(self._isotp, ISOTPFDTransceiver):
            self._isotp.configure_link(
                frame_length=frame_length,
                extended_addressing=uses_extended_addressing,
            )
            return
        self._isotp.frame_length = frame_length

    def _is_fd_transport(self) -> bool:
        return self._transport_profile == "isotp_fd"

    def _send_frame(self, bus: CANBusEmulation, arbitration_id: int, payload: List[int]) -> None:
        out_frame = CANFrame(
            arbitration_id=arbitration_id,
            data=payload,
            dlc=len(payload),
            is_fd=self._is_fd_transport(),
        )
        bus.write(out_frame, originator=self)

    @property
    def arbitration_ids(self):
        ids = set()
        for handler in self._handlers:
            ids.add(handler.arbitration_id)
            ids.update(handler.auxiliary_ids)
        if not ids:
            raise ValueError("No command handlers installed")
        return ids

    def add_command_handler(self, handler: CommandHandler) -> None:
        self._handlers.append(handler)
        self._refresh_isotp_parameters_from_handler(handler)

    def replace_command_handler(self, handler: CommandHandler) -> None:
        for index, existing in enumerate(self._handlers):
            if (
                existing.arbitration_id == handler.arbitration_id
                and existing.reply_id == handler.reply_id
                and existing.auxiliary_ids == handler.auxiliary_ids
            ):
                existing.unregister(self)
                self._handlers[index] = handler
                handler.register(self)
                self._refresh_isotp_parameters_from_handler(handler)
                return

    def configure_isotp_flow_control(
        self,
        wait_count: int = 0,
        wait_ms: float = 50.0,
        overflow: bool = False,
        wait_probability: float = 0.0,
        overflow_probability: float = 0.0,
        seed: Optional[int] = None,
    ) -> None:
        self._fc_wait_count = max(int(wait_count), 0)
        self._fc_wait_ms = max(float(wait_ms), 0.0)
        self._fc_overflow = bool(overflow)
        self._fc_wait_probability = max(float(wait_probability), 0.0)
        self._fc_overflow_probability = max(float(overflow_probability), 0.0)
        if seed is not None:
            self._fc_rng.seed(seed)

    def configure(self, property_name: str, value: str) -> None:
        for handler in self._handlers:
            handler.configure(property_name, value)

    def register(self, bus: CANBusEmulation) -> None:
        self._bus = bus
        for handler in self._handlers:
            handler.register(self)

    def unregister(self, bus: CANBusEmulation) -> None:
        for handler in self._handlers:
            handler.unregister(self)
        self._bus = None

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        with self._queue:
            self._feed(frame, bus)

    def _feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        if frame.arbitration_id not in self.arbitration_ids:
            return
        handler = self._command_handler(frame.arbitration_id)
        if handler is None:
            return

        # Collect all allowed request extensions for this handler
        allowed_extensions = set()
        if handler.request_extension is not None:
            allowed_extensions.add(handler.request_extension)
        allowed_extensions.update(handler.auxiliary_request_extensions)
        
        uses_extended_addressing = len(allowed_extensions) > 0
        
        if uses_extended_addressing:
            self._configure_isotp_link(uses_extended_addressing=True)
            # Check if the first byte matches any allowed extension
            if not frame.data or frame.data[0] not in allowed_extensions:
                return
        else:
            self._configure_isotp_link(uses_extended_addressing=False)

        payload = list(frame.data)
        if uses_extended_addressing and payload:
            payload = payload[1:]

        try:
            action = self._isotp.did_read(payload)
        except Exception:
            return

        if action.kind == "process":
            addressing = Addressing.unicast(
                identifier=frame.arbitration_id,
                reply=handler.reply_id,
                request_extension=handler.request_extension or 0,
                reply_extension=handler.reply_extension or 0,
            )
            message = Message(addressing=addressing, bytes=action.bytes or [])
            
            if message.bytes and (self.enable_logging or self.diagnostic_log_sink is not None):
                is_broadcast = (
                    uses_extended_addressing
                    and frame.data
                    and frame.data[0] in handler.auxiliary_request_extensions
                    and frame.data[0] != handler.request_extension
                )
                interpreted = interpret_diagnostic_message(
                    self.name,
                    message.bytes,
                    is_request=True,
                    is_broadcast=is_broadcast,
                )
                if self.diagnostic_log_sink is not None:
                    try:
                        self.diagnostic_log_sink(
                            {
                                "ecu": self.name,
                                "direction": "request",
                                "bytes": list(message.bytes),
                                "text": interpreted,
                                "timestamp": time.time(),
                            }
                        )
                    except Exception:
                        pass
                if self.enable_logging:
                    print(interpreted)
            
            response = self.handle(message, preferred_handler=handler)
            if response is None:
                return
                
            if response.bytes and (self.enable_logging or self.diagnostic_log_sink is not None):
                interpreted = interpret_diagnostic_message(self.name, response.bytes, is_request=False)
                if self.diagnostic_log_sink is not None:
                    try:
                        self.diagnostic_log_sink(
                            {
                                "ecu": self.name,
                                "direction": "response",
                                "bytes": list(response.bytes),
                                "text": interpreted,
                                "timestamp": time.time(),
                            }
                        )
                    except Exception:
                        pass
                if self.enable_logging:
                    print(interpreted)
            try:
                next_action = self._isotp.write(response.bytes)
            except Exception:
                return
            if next_action.kind == "write_frames":
                frames = list(next_action.frames or [])
                frames, followups = self._split_flow_control_frames(frames)
                for frame_bytes in frames:
                    payload = self._payload_by_prefixing(frame_bytes, handler.reply_extension or response.addressing.request_extension_optional)
                    self._send_frame(bus, response.id, payload)
                    time_sleep = next_action.separation_time / 1000.0
                    if time_sleep > 0:
                        threading.Event().wait(time_sleep)
                if followups:
                    self._schedule_flow_control_frames(bus, response.id, handler.reply_extension or response.addressing.request_extension_optional, followups)
            return

        if action.kind == "write_frames":
            frames = list(action.frames or [])
            frames, followups = self._split_flow_control_frames(frames)
            for frame_bytes in frames:
                payload = self._payload_by_prefixing(frame_bytes, handler.reply_extension)
                self._send_frame(bus, handler.reply_id, payload)
                time_sleep = action.separation_time / 1000.0
                if time_sleep > 0:
                    threading.Event().wait(time_sleep)
            if followups:
                self._schedule_flow_control_frames(bus, handler.reply_id, handler.reply_extension, followups)
            return

    def handle(self, message: Message, preferred_handler: Optional[CommandHandler] = None) -> Optional[Message]:
        if self.lock_status == LockStrategy.default:
            return None
        if self.lock_status == LockStrategy.bmw_nrc_forbidden:
            return message.negative_response(NegativeResponseCode.gateway_locked_communication)

        handlers = list(self._handlers)
        if preferred_handler in handlers:
            handlers.remove(preferred_handler)
            handlers.insert(0, preferred_handler)

        for handler in handlers:
            def intermediate_sink(intermediate_message: Message) -> None:
                if not self._bus:
                    return
                use_extended_addressing = handler.reply_extension is not None
                frame_width = self._effective_frame_length(use_extended_addressing)
                if self._is_fd_transport():
                    single_capacity = max(single_frame_payload_capacity_fd(frame_width), 0)
                else:
                    single_capacity = max(frame_width - 1, 0)
                if len(intermediate_message.bytes) <= 0 or len(intermediate_message.bytes) > single_capacity:
                    return

                isotp_payload: List[int]
                if self._is_fd_transport() and len(intermediate_message.bytes) > 0x07:
                    isotp_payload = [0x00, len(intermediate_message.bytes) & 0xFF] + list(intermediate_message.bytes)
                else:
                    isotp_payload = [len(intermediate_message.bytes)] + list(intermediate_message.bytes)

                if self._is_fd_transport():
                    target_width = min(
                        next_valid_fd_frame_length(
                            len(isotp_payload),
                            extended_addressing=use_extended_addressing,
                        ),
                        frame_width,
                    )
                else:
                    target_width = frame_width
                if len(isotp_payload) < target_width:
                    isotp_payload = isotp_payload + [0x00] * (target_width - len(isotp_payload))

                payload = self._payload_by_prefixing(isotp_payload, handler.reply_extension)
                self._send_frame(self._bus, intermediate_message.id, payload)
            response = handler.handle(message, intermediate_sink)
            if response is not None:
                return response
        return None

    def lock_all_other_ecus(self, strategy: LockStrategy) -> None:
        if self._bus:
            self._bus.lock_all_other_ecus(self, strategy)

    def unlock_all_other_ecus(self) -> None:
        if self._bus:
            self._bus.unlock_all_other_ecus(self)

    def perform_host_action(self, action) -> None:
        # Hook for UI/host automation.
        return

    def _command_handler(self, arbitration_id: int) -> Optional[CommandHandler]:
        for handler in self._handlers:
            if handler.arbitration_id == arbitration_id:
                return handler
            if arbitration_id in handler.auxiliary_ids:
                return handler
        return None

    def _payload_by_prefixing(self, payload: List[int], extension: Optional[int]) -> List[int]:
        if extension is None:
            return payload
        return [extension] + payload

    def _split_flow_control_frames(self, frames: List[List[int]]) -> tuple[List[List[int]], List[List[int]]]:
        if not frames:
            return frames, []
        first = frames[0]
        if not first:
            return frames, []
        if (first[0] & 0xF0) != 0x30:
            return frames, []
        if self._fc_overflow:
            overflow_frame = list(first)
            overflow_frame[0] = FlowStatus.overflow
            return [overflow_frame], []
        wait_count = self._fc_wait_count
        if wait_count <= 0 and (self._fc_wait_probability > 0.0 or self._fc_overflow_probability > 0.0):
            roll = self._fc_rng.random()
            if roll < self._fc_overflow_probability:
                overflow_frame = list(first)
                overflow_frame[0] = FlowStatus.overflow
                return [overflow_frame], []
            if roll < (self._fc_overflow_probability + self._fc_wait_probability):
                wait_count = max(self._fc_wait_count, 1)
        if wait_count <= 0:
            return frames, []
        wait_frame = list(first)
        wait_frame[0] = FlowStatus.wait
        cts_frame = list(first)
        cts_frame[0] = FlowStatus.clear_to_send
        followups: List[List[int]] = []
        if wait_count > 1:
            followups.extend([list(wait_frame) for _ in range(wait_count - 1)])
        followups.append(cts_frame)
        return [wait_frame], followups

    def _schedule_flow_control_frames(
        self,
        bus: CANBusEmulation,
        arbitration_id: int,
        extension: Optional[int],
        frames: List[List[int]],
    ) -> None:
        delay = self._fc_wait_ms / 1000.0

        def _send_at_index(index: int) -> None:
            if index >= len(frames):
                return
            payload = self._payload_by_prefixing(frames[index], extension)
            self._send_frame(bus, arbitration_id, payload)
            if index + 1 < len(frames):
                threading.Timer(delay, _send_at_index, args=(index + 1,)).start()

        if delay <= 0:
            for frame in frames:
                payload = self._payload_by_prefixing(frame, extension)
                self._send_frame(bus, arbitration_id, payload)
            return
        threading.Timer(delay, _send_at_index, args=(0,)).start()

    def __str__(self) -> str:
        return f"ECU '{self.name}' w/ handlers: {self._handlers}"
