from __future__ import annotations  # Enable postponed evaluation of annotations (PEP 563/649 style)

from typing import Optional  # For optional type hints

from .base import LinInterface, LinFrame  # Base abstraction and frame dataclass used by all LIN adapters


class BabyLinInterface(LinInterface):
    """LIN adapter that uses the vendor's BabyLIN Python SDK wrapper.

    - Avoids manual ctypes; relies on BabyLIN_library.py BLC_* functions.
    - Keeps the same LinInterface contract for send/receive/request/flush.
    """

    def __init__(
        self,
        dll_path: Optional[str] = None,  # Not used by SDK wrapper (auto-selects platform libs)
        bitrate: int = 19200,            # Informational; typically defined by SDF/schedule
        channel: int = 0,                # Channel index used with BLC_getChannelHandle (0-based)
        node_name: Optional[str] = None, # Optional friendly name (not used by SDK calls)
        func_names: Optional[dict] = None,  # Legacy (ctypes) compatibility; unused here
        sdf_path: Optional[str] = None,  # Optional SDF file to load after open
        schedule_nr: int = 0,            # Schedule number to start after connect
        wrapper_module: Optional[object] = None,  # Inject a wrapper (e.g., mock) for tests
    ) -> None:
        self.bitrate = bitrate                    # Store configured (informational) bitrate
        self.channel_index = channel              # Desired channel index
        self.node_name = node_name or "ECU_TEST_NODE"  # Default node name if not provided
        self.sdf_path = sdf_path                  # SDF to load (if provided)
        self.schedule_nr = schedule_nr            # Schedule to start on connect

        # Choose the BabyLIN wrapper module to use:
        # - If wrapper_module provided (unit tests with mock), use it
        # - Else dynamically import the real SDK wrapper (BabyLIN_library.py)
        if wrapper_module is not None:
            _bl = wrapper_module
        else:
            import importlib, sys, os  # Local import to avoid global dependency during unit tests
            _bl = None                 # Placeholder for resolved module
            import_errors = []         # Accumulate import errors for diagnostics
            for modname in ("BabyLIN_library", "vendor.BabyLIN_library"):
                try:
                    _bl = importlib.import_module(modname)
                    break
                except Exception as e:  # pragma: no cover
                    import_errors.append((modname, str(e)))
            if _bl is None:
                # Try adding the common 'vendor' folder to sys.path then retry import
                repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
                vendor_dir = os.path.join(repo_root, "vendor")
                if os.path.isdir(vendor_dir) and vendor_dir not in sys.path:
                    sys.path.insert(0, vendor_dir)
                    try:
                        _bl = importlib.import_module("BabyLIN_library")
                    except Exception as e:  # pragma: no cover
                        import_errors.append(("BabyLIN_library", str(e)))
            if _bl is None:
                # Raise a helpful error with all attempted import paths
                details = "; ".join([f"{m}: {err}" for m, err in import_errors]) or "not found"
                raise RuntimeError(
                    "Failed to import BabyLIN_library. Ensure the SDK's BabyLIN_library.py is present in the project (e.g., vendor/BabyLIN_library.py). Details: "
                    + details
                )

        # Create the BabyLIN SDK instance (module exposes create_BabyLIN())
        self._BabyLIN = _bl.create_BabyLIN()
        # Small helper to call BLC_* functions by name (keeps call sites concise)
        self._bl_call = lambda name, *args, **kwargs: getattr(self._BabyLIN, name)(*args, **kwargs)

        self._handle = None          # Device handle returned by BLC_openPort
        self._channel_handle = None  # Per-channel handle returned by BLC_getChannelHandle
        self._connected = False      # Internal connection state flag

    def _err(self, rc: int) -> None:
        """Raise a RuntimeError with a readable SDK error message for rc != BL_OK."""
        if rc == self._BabyLIN.BL_OK:
            return
        # Prefer a human-friendly error string if the SDK provides it
        try:
            get_str = getattr(self._BabyLIN, 'BLC_getDetailedErrorString', None)
            msg = get_str(rc) if get_str else f"rc={rc}"
            if not isinstance(msg, str):
                msg = str(msg)
        except Exception:
            msg = f"rc={rc}"
        raise RuntimeError(f"BabyLIN error: {msg}")

    def connect(self) -> None:
        """Open device, optionally load SDF, select channel, and start schedule."""
        # Discover BabyLIN devices (returns a list of port identifiers)
        ports = self._bl_call('BLC_getBabyLinPorts', 100)
        if not ports:
            raise RuntimeError("No BabyLIN devices found")
        # Open the first available device port (you could extend to select by config)
        self._handle = self._bl_call('BLC_openPort', ports[0])
        if not self._handle:
            raise RuntimeError("Failed to open BabyLIN port")

        # Load SDF onto the device, if configured (3rd arg '1' often means 'download')
        if self.sdf_path:
            rc = self._bl_call('BLC_loadSDF', self._handle, self.sdf_path, 1)
            if rc != self._BabyLIN.BL_OK:
                self._err(rc)

        # Get channel count and pick the configured channel index (default 0)
        ch_count = self._bl_call('BLC_getChannelCount', self._handle)
        if ch_count <= 0:
            raise RuntimeError("No channels reported by device")
        ch_idx = int(self.channel_index)
        if ch_idx < 0 or ch_idx >= ch_count:
            ch_idx = 0
        # Resolve a channel handle used for all subsequent Tx/Rx commands
        self._channel_handle = self._bl_call('BLC_getChannelHandle', self._handle, ch_idx)

        # Start a schedule if configured (common requirement for regular polling/masters)
        if self.schedule_nr is not None:
            cmd = f"start schedule {int(self.schedule_nr)};"
            rc = self._bl_call('BLC_sendCommand', self._channel_handle, cmd)
            if rc != self._BabyLIN.BL_OK:
                self._err(rc)

        self._connected = True  # Mark interface as connected

    def disconnect(self) -> None:
        """Close device handles and reset internal state (best-effort)."""
        try:
            self._bl_call('BLC_closeAll')  # Close all device connections via SDK
        except Exception:
            pass  # Ignore SDK exceptions during shutdown
        self._connected = False
        self._handle = None
        self._channel_handle = None

    def send(self, frame: LinFrame) -> None:
        """Transmit a LIN frame using BLC_mon_set_xmit."""
        if not self._connected or not self._channel_handle:
            raise RuntimeError("BabyLIN not connected")
        # slotTime=0 means use default timing configured by schedule/SDF
        rc = self._bl_call('BLC_mon_set_xmit', self._channel_handle, int(frame.id), bytes(frame.data), 0)
        if rc != self._BabyLIN.BL_OK:
            self._err(rc)

    def receive(self, id: Optional[int] = None, timeout: float = 1.0):
        """Receive a LIN frame with optional ID filter and timeout (seconds)."""
        if not self._connected or not self._channel_handle:
            raise RuntimeError("BabyLIN not connected")
        ms = max(0, int(timeout * 1000))  # SDK expects milliseconds
        try:
            frame = self._bl_call('BLC_getNextFrameTimeout', self._channel_handle, ms)
        except Exception:
            # Many wrappers raise on timeout; unify as 'no data'
            return None
        if not frame:
            return None
        # Convert SDK frame to our LinFrame (mask to classic 6-bit LIN ID range)
        fid = int(frame.frameId & 0x3F)
        data = bytes(list(frame.frameData)[: int(frame.lenOfData)])
        lin_frame = LinFrame(id=fid, data=data)
        if id is None or fid == id:
            return lin_frame
        # If a different ID was received and caller requested a filter, return None
        return None

    def flush(self) -> None:
        """Flush RX buffers if the SDK exposes such a function (optional)."""
        if not self._connected or not self._channel_handle:
            return
        try:
            # Some SDKs may not expose flush; no-op if missing
            flush = getattr(self._BabyLIN, 'BLC_flush', None)
            if flush:
                flush(self._channel_handle)
        except Exception:
            pass

    def request(self, id: int, length: int, timeout: float = 1.0):
        """Perform a LIN master request and wait for response.

        Strategy:
        - Prefer SDK method `BLC_sendRawMasterRequest` if present (bytes or length variants).
        - Fallback: transmit a header with zeroed payload; then wait for response.
        - Always attempt to receive a frame with matching ID within 'timeout'.
        """
        if not self._connected or not self._channel_handle:
            raise RuntimeError("BabyLIN not connected")

        sent = False  # Track whether a request command was successfully issued
    # Attempt to use raw master request if provided by SDK
    # Preference: try (channel, frameId, length) first because our mock wrapper
    # synthesizes a deterministic payload for this form (see vendor/mock_babylin_wrapper.py),
    # then fall back to (channel, frameId, dataBytes) if the SDK only supports that.
        raw_req = getattr(self._BabyLIN, 'BLC_sendRawMasterRequest', None)
        if raw_req:
            # Prefer the (channel, frameId, length) variant first if supported
            try:
                rc = raw_req(self._channel_handle, int(id), int(length))
                if rc == self._BabyLIN.BL_OK:
                    sent = True
                else:
                    self._err(rc)
            except TypeError:
                # Fallback to (channel, frameId, dataBytes)
                try:
                    payload = bytes([0] * max(0, min(8, int(length))))
                    rc = raw_req(self._channel_handle, int(id), payload)
                    if rc == self._BabyLIN.BL_OK:
                        sent = True
                    else:
                        self._err(rc)
                except Exception:
                    sent = False
            except Exception:
                sent = False

        if not sent:
            # Fallback: issue a transmit; many stacks will respond on the bus
            self.send(LinFrame(id=id, data=bytes([0] * max(0, min(8, int(length))))))

        # Wait for the response frame with matching ID (or None on timeout)
        return self.receive(id=id, timeout=timeout)
