"""Support for TPLink sensor entities."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from operator import methodcaller
from typing import TYPE_CHECKING, Any, cast

from kasa import Feature
from kasa.smart.modules.clean import ErrorCode as VacuumError

from homeassistant.components.sensor import (
    DOMAIN as SENSOR_DOMAIN,
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import TPLinkConfigEntry
from .const import UNIT_MAPPING
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription


@dataclass(frozen=True, kw_only=True)
class TPLinkSensorEntityDescription(
    SensorEntityDescription, TPLinkFeatureEntityDescription
):
    """Base class for a TPLink feature based sensor entity description."""

    #: Optional callable to convert the value
    convert_fn: Callable[[Any], Any] | None = None


# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0

_TOTAL_SECONDS_METHOD_CALLER = methodcaller("total_seconds")

SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = (
    TPLinkSensorEntityDescription(
        key="current_consumption",
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="consumption_total",
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
    ),
    TPLinkSensorEntityDescription(
        key="consumption_today",
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
    ),
    TPLinkSensorEntityDescription(
        key="consumption_this_month",
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
    ),
    TPLinkSensorEntityDescription(
        key="voltage",
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="current",
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="temperature",
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        # Disable as the value reported by the device changes seconds frequently
        entity_registry_enabled_default=False,
        key="on_since",
        device_class=SensorDeviceClass.TIMESTAMP,
    ),
    TPLinkSensorEntityDescription(
        key="rssi",
        device_class=SensorDeviceClass.SIGNAL_STRENGTH,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="signal_level",
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="ssid",
    ),
    TPLinkSensorEntityDescription(
        key="battery_level",
        device_class=SensorDeviceClass.BATTERY,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="auto_off_at",
        device_class=SensorDeviceClass.TIMESTAMP,
    ),
    TPLinkSensorEntityDescription(
        key="device_time",
        device_class=SensorDeviceClass.TIMESTAMP,
    ),
    TPLinkSensorEntityDescription(
        key="water_alert_timestamp",
        device_class=SensorDeviceClass.TIMESTAMP,
    ),
    TPLinkSensorEntityDescription(
        key="humidity",
        device_class=SensorDeviceClass.HUMIDITY,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="report_interval",
        device_class=SensorDeviceClass.DURATION,
    ),
    TPLinkSensorEntityDescription(
        key="alarm_source",
    ),
    # Vacuum cleaning records
    TPLinkSensorEntityDescription(
        key="clean_time",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.MINUTES,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        key="clean_area",
        device_class=SensorDeviceClass.AREA,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="clean_progress",
        state_class=SensorStateClass.MEASUREMENT,
    ),
    TPLinkSensorEntityDescription(
        key="last_clean_time",
        device_class=SensorDeviceClass.DURATION,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.MINUTES,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        key="last_clean_area",
        device_class=SensorDeviceClass.AREA,
    ),
    TPLinkSensorEntityDescription(
        key="last_clean_timestamp",
        device_class=SensorDeviceClass.TIMESTAMP,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="total_clean_time",
        device_class=SensorDeviceClass.DURATION,
        state_class=SensorStateClass.TOTAL_INCREASING,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.MINUTES,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="total_clean_area",
        device_class=SensorDeviceClass.AREA,
        state_class=SensorStateClass.TOTAL_INCREASING,
    ),
    TPLinkSensorEntityDescription(
        key="total_clean_count",
        state_class=SensorStateClass.TOTAL_INCREASING,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="main_brush_remaining",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="main_brush_used",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="side_brush_remaining",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="side_brush_used",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="filter_remaining",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="filter_used",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="sensor_remaining",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="sensor_used",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="charging_contacts_remaining",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        entity_registry_enabled_default=False,
        key="charging_contacts_used",
        device_class=SensorDeviceClass.DURATION,
        native_unit_of_measurement=UnitOfTime.SECONDS,
        suggested_unit_of_measurement=UnitOfTime.HOURS,
        convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
    ),
    TPLinkSensorEntityDescription(
        key="vacuum_error",
        device_class=SensorDeviceClass.ENUM,
        options=[name.lower() for name in VacuumError._member_names_],
        convert_fn=lambda x: x.name.lower(),
    ),
)

SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS}


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: TPLinkConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up sensors."""
    data = config_entry.runtime_data
    parent_coordinator = data.parent_coordinator
    device = parent_coordinator.device
    known_child_device_ids: set[str] = set()
    first_check = True

    def _check_device() -> None:
        entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
            hass=hass,
            device=device,
            coordinator=parent_coordinator,
            feature_type=Feature.Type.Sensor,
            entity_class=TPLinkSensorEntity,
            descriptions=SENSOR_DESCRIPTIONS_MAP,
            platform_domain=SENSOR_DOMAIN,
            known_child_device_ids=known_child_device_ids,
            first_check=first_check,
        )
        async_add_entities(entities)

    _check_device()
    first_check = False
    config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))


class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity):
    """Representation of a feature-based TPLink sensor."""

    entity_description: TPLinkSensorEntityDescription

    @callback
    def _async_update_attrs(self) -> bool:
        """Update the entity's attributes."""
        value = self._feature.value
        if value is not None and self._feature.precision_hint is not None:
            value = round(cast(float, value), self._feature.precision_hint)
            # We probably do not need this, when we are rounding already?
            self._attr_suggested_display_precision = self._feature.precision_hint

        if self.entity_description.convert_fn:
            value = self.entity_description.convert_fn(value)

        if TYPE_CHECKING:
            from datetime import date, datetime  # noqa: PLC0415

            assert isinstance(value, str | int | float | date | datetime | None)

        self._attr_native_value = value
        # Map to homeassistant units and fallback to upstream one if none found
        if (unit := self._feature.unit) is not None:
            self._attr_native_unit_of_measurement = UNIT_MAPPING.get(unit, unit)
        return True
