"""The Meater Temperature Probe integration."""

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import override

from meater.MeaterApi import MeaterProbe

from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util

from . import MeaterCoordinator
from .const import DOMAIN, MEATER_DATA
from .coordinator import MeaterConfigEntry

COOK_STATES = {
    "Not Started": "not_started",
    "Configured": "configured",
    "Started": "started",
    "Ready For Resting": "ready_for_resting",
    "Resting": "resting",
    "Slightly Underdone": "slightly_underdone",
    "Finished": "finished",
    "Slightly Overdone": "slightly_overdone",
    "OVERCOOK!": "overcooked",
}


@dataclass(frozen=True, kw_only=True)
class MeaterSensorEntityDescription(SensorEntityDescription):
    """Describes meater sensor entity."""

    value: Callable[[MeaterProbe], datetime | float | str | None]
    unavailable_when_not_cooking: bool = False


def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
    """Convert elapsed time to timestamp."""
    if not probe.cook or not hasattr(probe.cook, "time_elapsed"):
        return None
    return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed)


def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
    """Convert remaining time to timestamp."""
    if (
        not probe.cook
        or not hasattr(probe.cook, "time_remaining")
        or probe.cook.time_remaining < 0
    ):
        return None
    return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining)


SENSOR_TYPES = (
    # Ambient temperature
    MeaterSensorEntityDescription(
        key="ambient",
        translation_key="ambient",
        device_class=SensorDeviceClass.TEMPERATURE,
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        state_class=SensorStateClass.MEASUREMENT,
        value=lambda probe: probe.ambient_temperature,
    ),
    # Internal temperature (probe tip)
    MeaterSensorEntityDescription(
        key="internal",
        translation_key="internal",
        device_class=SensorDeviceClass.TEMPERATURE,
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        state_class=SensorStateClass.MEASUREMENT,
        value=lambda probe: probe.internal_temperature,
    ),
    # Name of selected meat in user language or user given custom name
    MeaterSensorEntityDescription(
        key="cook_name",
        translation_key="cook_name",
        unavailable_when_not_cooking=True,
        value=lambda probe: probe.cook.name if probe.cook else None,
    ),
    MeaterSensorEntityDescription(
        key="cook_state",
        translation_key="cook_state",
        unavailable_when_not_cooking=True,
        device_class=SensorDeviceClass.ENUM,
        options=list(COOK_STATES.values()),
        value=lambda probe: COOK_STATES.get(probe.cook.state) if probe.cook else None,
    ),
    # Target temperature
    MeaterSensorEntityDescription(
        key="cook_target_temp",
        translation_key="cook_target_temp",
        device_class=SensorDeviceClass.TEMPERATURE,
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        state_class=SensorStateClass.MEASUREMENT,
        unavailable_when_not_cooking=True,
        value=(
            lambda probe: (
                probe.cook.target_temperature
                if probe.cook and hasattr(probe.cook, "target_temperature")
                else None
            )
        ),
    ),
    # Peak temperature
    MeaterSensorEntityDescription(
        key="cook_peak_temp",
        translation_key="cook_peak_temp",
        device_class=SensorDeviceClass.TEMPERATURE,
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        state_class=SensorStateClass.MEASUREMENT,
        unavailable_when_not_cooking=True,
        value=(
            lambda probe: (
                probe.cook.peak_temperature
                if probe.cook and hasattr(probe.cook, "peak_temperature")
                else None
            )
        ),
    ),
    # Remaining time in seconds. When unknown/calculating default is used. Default: -1
    # Exposed as a TIMESTAMP sensor where the timestamp is
    # current time + remaining time.
    MeaterSensorEntityDescription(
        key="cook_time_remaining",
        translation_key="cook_time_remaining",
        device_class=SensorDeviceClass.TIMESTAMP,
        unavailable_when_not_cooking=True,
        value=_remaining_time_to_timestamp,
    ),
    # Time since the start of cook in seconds. Default: 0. Exposed as a TIMESTAMP sensor
    # where the timestamp is current time - elapsed time.
    MeaterSensorEntityDescription(
        key="cook_time_elapsed",
        translation_key="cook_time_elapsed",
        device_class=SensorDeviceClass.TIMESTAMP,
        unavailable_when_not_cooking=True,
        value=_elapsed_time_to_timestamp,
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    entry: MeaterConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the entry."""
    coordinator = entry.runtime_data

    @callback
    def async_update_data():
        """Handle updated data from the API endpoint."""
        if not coordinator.last_update_success:
            return None

        devices = coordinator.data
        entities = []
        known_probes = hass.data[MEATER_DATA]

        # Add entities for temperature probes which we've not yet seen
        for device_id in devices:
            if device_id in known_probes:
                continue

            entities.extend(
                [
                    MeaterProbeTemperature(coordinator, device_id, sensor_description)
                    for sensor_description in SENSOR_TYPES
                ]
            )
            known_probes.add(device_id)

        async_add_entities(entities)

        return devices

    # Add a subscriber to the coordinator to discover new temperature probes
    coordinator.async_add_listener(async_update_data)
    async_update_data()


class MeaterProbeTemperature(SensorEntity, CoordinatorEntity[MeaterCoordinator]):
    """Meater Temperature Sensor Entity."""

    _attr_has_entity_name = True
    entity_description: MeaterSensorEntityDescription

    def __init__(
        self,
        coordinator: MeaterCoordinator,
        device_id: str,
        description: MeaterSensorEntityDescription,
    ) -> None:
        """Initialise the sensor."""
        super().__init__(coordinator)
        self._attr_device_info = DeviceInfo(
            identifiers={
                # Serial numbers are unique identifiers within a specific domain
                (DOMAIN, device_id)
            },
            manufacturer="Apption Labs",
            model="Meater Probe",
            name=f"Meater Probe {device_id[:8]}",
        )
        self._attr_unique_id = f"{device_id}-{description.key}"

        self.device_id = device_id
        self.entity_description = description

    @property
    def probe(self) -> MeaterProbe:
        """Return the probe."""
        return self.coordinator.data[self.device_id]

    @property
    @override
    def native_value(self) -> datetime | float | str | None:
        """Return the temperature of the probe."""
        return self.entity_description.value(self.probe)

    @property
    @override
    def available(self) -> bool:
        """Return if entity is available."""
        # See if the device was returned from the API. If not, it's offline
        return (
            super().available
            and self.device_id in self.coordinator.data
            and (
                not self.entity_description.unavailable_when_not_cooking
                or self.probe.cook is not None
            )
        )
