"""
Build Sentinel sensors from a JSON hardware bench configuration.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path

from sentinel.sensors.base import CurrentSensor, TemperatureSensor
from sentinel.sensors.ds18b20 import (
    DS18B20TemperatureSensor,
    discover_ds18b20,
)
from sentinel.sensors.max31865 import MAX31865TemperatureSensor
from sentinel.sensors.simulated import (
    SimulatedCurrentSensor,
    SimulatedTemperatureSensor,
)


class HardwareConfigurationError(RuntimeError):
    """Raised when the hardware configuration is invalid."""


@dataclass(frozen=True)
class HardwareSensorSuite:
    """All five sensor channels required by AcquisitionService."""

    ambient_sensor: TemperatureSensor
    internal_sensor: TemperatureSensor
    evaporator_sensor: TemperatureSensor
    compressor_sensor: TemperatureSensor
    current_sensor: CurrentSensor


def load_hardware_config(
    config_path: str | Path,
) -> dict[str, object]:
    """Load a JSON hardware configuration file."""
    path = Path(config_path)

    try:
        return json.loads(
            path.read_text(
                encoding="utf-8",
            )
        )
    except (OSError, json.JSONDecodeError) as error:
        raise HardwareConfigurationError(
            f"Unable to load hardware configuration: {path}"
        ) from error


def _resolve_ds18b20_id(
    configured_id: str,
) -> str:
    """
    Resolve AUTO_1 / AUTO_2 from detected 1-Wire devices.
    """
    if not configured_id.startswith("AUTO_"):
        return configured_id

    try:
        index = int(
            configured_id.split(
                "_",
                maxsplit=1,
            )[1]
        ) - 1
    except (IndexError, ValueError) as error:
        raise HardwareConfigurationError(
            f"Invalid automatic DS18B20 ID: {configured_id}"
        ) from error

    detected = discover_ds18b20()

    if index < 0 or index >= len(detected):
        raise HardwareConfigurationError(
            f"{configured_id} requested but only "
            f"{len(detected)} DS18B20 sensor(s) detected: "
            f"{detected}"
        )

    return detected[index]


def _build_temperature_sensor(
    config: dict[str, object],
) -> TemperatureSensor:
    """Build one temperature sensor from configuration."""
    sensor_type = str(
        config.get("type", "")
    )

    if sensor_type == "ds18b20":
        configured_id = str(
            config.get(
                "device_id",
                "",
            )
        )

        device_id = _resolve_ds18b20_id(
            configured_id
        )

        return DS18B20TemperatureSensor(
            device_id=device_id,
        )

    if sensor_type == "max31865":
        return MAX31865TemperatureSensor(
            spi_bus=int(
                config.get(
                    "spi_bus",
                    0,
                )
            ),
            spi_device=int(
                config.get(
                    "spi_device",
                    0,
                )
            ),
            wires=int(
                config.get(
                    "wires",
                    3,
                )
            ),
            rtd_nominal_ohms=float(
                config.get(
                    "rtd_nominal_ohms",
                    100.0,
                )
            ),
            reference_resistor_ohms=float(
                config.get(
                    "reference_resistor_ohms",
                    430.0,
                )
            ),
            filter_hz=int(
                config.get(
                    "filter_hz",
                    50,
                )
            ),
        )

    if sensor_type == "simulated_temperature":
        return SimulatedTemperatureSensor(
            reference_temperature_c=float(
                config.get(
                    "reference_c",
                    22.0,
                )
            ),
            variation_c=float(
                config.get(
                    "variation_c",
                    0.05,
                )
            ),
        )

    raise HardwareConfigurationError(
        f"Unsupported temperature sensor type: "
        f"{sensor_type}"
    )


def _build_current_sensor(
    config: dict[str, object],
) -> CurrentSensor:
    """Build one current sensor from configuration."""
    sensor_type = str(
        config.get("type", "")
    )

    if sensor_type == "simulated_current":
        return SimulatedCurrentSensor(
            reference_current_a=float(
                config.get(
                    "reference_a",
                    0.0,
                )
            ),
            variation_a=float(
                config.get(
                    "variation_a",
                    0.0,
                )
            ),
        )

    raise HardwareConfigurationError(
        f"Unsupported current sensor type: "
        f"{sensor_type}"
    )


def build_hardware_sensor_suite(
    config: dict[str, object],
) -> HardwareSensorSuite:
    """Build all Sentinel acquisition channels."""
    channels = config.get("channels")

    if not isinstance(channels, dict):
        raise HardwareConfigurationError(
            "The configuration must contain a 'channels' object."
        )

    required = (
        "ambient",
        "internal",
        "evaporator",
        "compressor",
        "current",
    )

    missing = [
        name
        for name in required
        if name not in channels
    ]

    if missing:
        raise HardwareConfigurationError(
            "Missing channel configuration: "
            + ", ".join(missing)
        )

    return HardwareSensorSuite(
        ambient_sensor=_build_temperature_sensor(
            channels["ambient"]
        ),
        internal_sensor=_build_temperature_sensor(
            channels["internal"]
        ),
        evaporator_sensor=_build_temperature_sensor(
            channels["evaporator"]
        ),
        compressor_sensor=_build_temperature_sensor(
            channels["compressor"]
        ),
        current_sensor=_build_current_sensor(
            channels["current"]
        ),
    )
