"""
DS18B20 temperature sensor driver for Sentinel.

The driver uses the Linux 1-Wire sysfs interface. It therefore needs
no third-party Python package.
"""

from pathlib import Path

from sentinel.sensors.base import TemperatureSensor


class DS18B20Error(RuntimeError):
    """Raised when a DS18B20 reading cannot be obtained."""


class DS18B20TemperatureSensor(TemperatureSensor):
    """
    Read one DS18B20 sensor through Linux 1-Wire sysfs.
    """

    DEFAULT_W1_ROOT = Path("/sys/bus/w1/devices")

    def __init__(
        self,
        device_id: str,
        w1_root: str | Path | None = None,
    ) -> None:
        """
        Initialize a DS18B20 sensor.

        Args:
            device_id: Linux 1-Wire device ID, for example
                ``28-00000abcdef0``.
            w1_root: Optional sysfs root, primarily useful for tests.
        """
        self.device_id = device_id
        self.w1_root = Path(
            w1_root
            if w1_root is not None
            else self.DEFAULT_W1_ROOT
        )

    @property
    def data_path(self) -> Path:
        """Return the DS18B20 sysfs data file path."""
        return (
            self.w1_root
            / self.device_id
            / "w1_slave"
        )

    def read_temperature_c(self) -> float:
        """
        Read and return the temperature in degrees Celsius.
        """
        try:
            lines = self.data_path.read_text(
                encoding="utf-8",
            ).strip().splitlines()
        except OSError as error:
            raise DS18B20Error(
                f"Unable to read DS18B20 {self.device_id}: "
                f"{self.data_path}"
            ) from error

        if len(lines) < 2:
            raise DS18B20Error(
                f"Invalid DS18B20 response from {self.device_id}."
            )

        if not lines[0].strip().endswith("YES"):
            raise DS18B20Error(
                f"CRC check failed for DS18B20 {self.device_id}."
            )

        marker = "t="
        marker_index = lines[1].find(marker)

        if marker_index < 0:
            raise DS18B20Error(
                f"Temperature value missing for {self.device_id}."
            )

        raw_value = lines[1][
            marker_index + len(marker):
        ]

        try:
            temperature_c = int(raw_value) / 1000.0
        except ValueError as error:
            raise DS18B20Error(
                f"Invalid temperature value for {self.device_id}."
            ) from error

        return round(
            temperature_c,
            3,
        )


def discover_ds18b20(
    w1_root: str | Path | None = None,
) -> list[str]:
    """
    Return all detected DS18B20 device IDs.
    """
    root = Path(
        w1_root
        if w1_root is not None
        else DS18B20TemperatureSensor.DEFAULT_W1_ROOT
    )

    if not root.exists():
        return []

    return sorted(
        path.name
        for path in root.glob("28-*")
        if (
            path.is_dir()
            and (path / "w1_slave").exists()
        )
    )
