"""LIN adapter that uses the Melexis Universal Master (MUM) over the network.

Wraps the vendor's `pylin` + `pymumclient` packages so test code can talk to
the MUM through the same `LinInterface` abstraction used by the BabyLIN and
mock adapters. The MUM is a BeagleBone-based LIN master reachable over IP
(default 192.168.7.2) with built-in power control on `power_out0`.

The MUM is master-driven: a slave frame is fetched by issuing a request via
`send_message(master_to_slave=False, frame_id, data_length)`, so `receive()`
requires a frame ID. Per-frame `data_length` is taken from the constructor's
`frame_lengths` map; ALM_Status (0x11, 4 bytes) and ALM_Req_A (0x0A, 8 bytes)
have built-in defaults so the common cases work out of the box.

Diagnostic frames (BSM-SNPD) need the LIN 1.x **Classic** checksum, which
`send_message` does not produce. Use `send_raw()` (which calls the transport
layer's `ld_put_raw`) for those frames.
"""
from __future__ import annotations

import time
from typing import Dict, Optional

from .base import LinInterface, LinFrame


# Sensible defaults for the 4SEVEN_color_lib_test ECU. Callers can extend or
# override these via the `frame_lengths` constructor argument.
_DEFAULT_FRAME_LENGTHS: Dict[int, int] = {
    0x0A: 8,   # ALM_Req_A   (master-published, RGB control)
    0x11: 4,   # ALM_Status  (slave-published)
    0x06: 3,   # ConfigFrame (master-published)
    0x12: 8,   # PWM_Frame   (slave-published)
    0x13: 8,   # VF_Frame    (slave-published)
    0x14: 8,   # Tj_Frame    (slave-published)
    0x15: 8,   # PWM_wo_Comp (slave-published)
    0x16: 8,   # NVM_Debug   (slave-published)
}


class MumLinInterface(LinInterface):
    """LIN adapter for the Melexis Universal Master."""

    def __init__(
        self,
        host: str = "192.168.7.2",
        lin_device: str = "lin0",
        power_device: str = "power_out0",
        baudrate: int = 19200,
        frame_lengths: Optional[Dict[int, int]] = None,
        default_data_length: int = 8,
        boot_settle_seconds: float = 0.5,
        # Test seam: inject pre-built modules to bypass real hardware.
        mum_module: object = None,
        pylin_module: object = None,
    ) -> None:
        self.host = host
        self.lin_device = lin_device
        self.power_device = power_device
        self.baudrate = int(baudrate)
        self.boot_settle_seconds = float(boot_settle_seconds)
        self.default_data_length = int(default_data_length)
        self.frame_lengths = dict(_DEFAULT_FRAME_LENGTHS)
        if frame_lengths:
            self.frame_lengths.update({int(k): int(v) for k, v in frame_lengths.items()})

        self._mum_module = mum_module
        self._pylin_module = pylin_module

        self._mum = None
        self._linmaster = None
        self._power_control = None
        self._lin_dev = None
        self._transport_layer = None
        self._connected = False

    # -----------------------------
    # Lifecycle
    # -----------------------------
    def _resolve_modules(self):
        """Lazy-import MUM stack so the framework still loads without it."""
        if self._mum_module is None:
            try:
                import pymumclient  # type: ignore
            except Exception as e:
                raise RuntimeError(
                    "pymumclient is not installed. The MUM adapter requires Melexis "
                    "packages 'pymumclient' and 'pylin'. See "
                    "vendor/automated_lin_test/install_packages.sh."
                ) from e
            self._mum_module = pymumclient
        if self._pylin_module is None:
            try:
                import pylin  # type: ignore
            except Exception as e:
                raise RuntimeError(
                    "pylin is not installed. The MUM adapter requires Melexis "
                    "packages 'pymumclient' and 'pylin'. See "
                    "vendor/automated_lin_test/install_packages.sh."
                ) from e
            self._pylin_module = pylin
        return self._mum_module, self._pylin_module

    def connect(self) -> None:
        """Open MUM, set up LIN master, attach LIN bus, and power up the ECU."""
        pymumclient, pylin = self._resolve_modules()

        self._mum = pymumclient.MelexisUniversalMaster()
        self._mum.open_all(self.host)

        self._power_control = self._mum.get_device(self.power_device)
        self._linmaster = self._mum.get_device(self.lin_device)
        self._linmaster.setup()

        lin_bus = pylin.LinBusManager(self._linmaster)
        self._lin_dev = pylin.LinDevice22(lin_bus)
        self._lin_dev.baudrate = self.baudrate

        # Transport layer is needed for Classic-checksum diagnostic frames.
        try:
            self._transport_layer = self._lin_dev.get_device("bus/transport_layer")
        except Exception:
            self._transport_layer = None

        # Power up and let the ECU boot before the first frame.
        self._power_control.power_up()
        if self.boot_settle_seconds > 0:
            time.sleep(self.boot_settle_seconds)

        self._connected = True

    def disconnect(self) -> None:
        """Power down the ECU and tear down the MUM connection (best-effort)."""
        if self._power_control is not None:
            try:
                self._power_control.power_down()
            except Exception:
                pass
        if self._linmaster is not None:
            try:
                self._linmaster.teardown()
            except Exception:
                pass
        self._connected = False
        self._mum = None
        self._linmaster = None
        self._power_control = None
        self._lin_dev = None
        self._transport_layer = None

    # -----------------------------
    # LinInterface contract
    # -----------------------------
    def send(self, frame: LinFrame) -> None:
        """Publish a master-to-slave frame using Enhanced checksum."""
        if not self._connected or self._lin_dev is None:
            raise RuntimeError("MUM not connected")
        self._lin_dev.send_message(
            master_to_slave=True,
            frame_id=int(frame.id),
            data_length=len(frame.data),
            data=list(frame.data),
        )

    def receive(self, id: Optional[int] = None, timeout: float = 1.0) -> Optional[LinFrame]:
        """Trigger a slave-to-master read for `id` and return the response.

        The MUM is master-driven, so a frame ID is required; passing None
        raises NotImplementedError. `timeout` is informational only — the
        underlying pylin call is synchronous and uses its own timing.
        """
        if not self._connected or self._lin_dev is None:
            raise RuntimeError("MUM not connected")
        if id is None:
            raise NotImplementedError(
                "MUM receive requires a frame ID; passive listen is not supported"
            )
        length = self.frame_lengths.get(int(id), self.default_data_length)
        try:
            response = self._lin_dev.send_message(
                master_to_slave=False,
                frame_id=int(id),
                data_length=int(length),
                data=None,
            )
        except Exception:
            return None  # treat any pylin exception as a timeout / no-data
        if not response:
            return None
        return LinFrame(id=int(id) & 0x3F, data=bytes(response[: int(length)]))

    # -----------------------------
    # MUM-specific extras
    # -----------------------------
    def send_raw(self, data: bytes) -> None:
        """Send a raw LIN frame using LIN 1.x **Classic** checksum.

        Required for BSM-SNPD diagnostic frames (service ID 0xB5) — the
        firmware rejects these if Enhanced checksum is used.
        """
        if not self._connected or self._transport_layer is None:
            raise RuntimeError("MUM transport layer not available")
        self._transport_layer.ld_put_raw(data=bytearray(data), baudrate=self.baudrate)

    def power_up(self) -> None:
        if self._power_control is None:
            raise RuntimeError("MUM not connected")
        self._power_control.power_up()

    def power_down(self) -> None:
        if self._power_control is None:
            raise RuntimeError("MUM not connected")
        self._power_control.power_down()

    def power_cycle(self, wait: float = 2.0) -> None:
        """Power the ECU down, wait `wait` seconds, then back up."""
        self.power_down()
        time.sleep(wait)
        self.power_up()
        if self.boot_settle_seconds > 0:
            time.sleep(self.boot_settle_seconds)
