"""Support for control of ElkM1 sensors."""

from typing import Any, cast, override

from elkm1_lib.const import SettingFormat, ZoneType
from elkm1_lib.counters import Counter
from elkm1_lib.elements import Element
from elkm1_lib.keypads import Keypad
from elkm1_lib.panel import Panel
from elkm1_lib.settings import Setting
from elkm1_lib.util import pretty_const
from elkm1_lib.zones import Zone
import voluptuous as vol

from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorStateClass,
)
from homeassistant.const import EntityCategory, UnitOfElectricPotential
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform, entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import VolDictType

from . import ElkM1ConfigEntry
from .const import ATTR_VALUE, ELK_USER_CODE_SERVICE_SCHEMA
from .entity import (
    ElkAttachedEntity,
    ElkEntity,
    create_elk_entities,
    generate_unique_id,
)
from .util import deprecate_entity

SERVICE_SENSOR_COUNTER_REFRESH = "sensor_counter_refresh"
SERVICE_SENSOR_COUNTER_SET = "sensor_counter_set"
SERVICE_SENSOR_ZONE_BYPASS = "sensor_zone_bypass"
SERVICE_SENSOR_ZONE_TRIGGER = "sensor_zone_trigger"
UNDEFINED_TEMPERATURE = -40

_DEVICE_CLASS_MAP: dict[ZoneType, SensorDeviceClass] = {
    ZoneType.TEMPERATURE: SensorDeviceClass.TEMPERATURE,
    ZoneType.ANALOG_ZONE: SensorDeviceClass.VOLTAGE,
}

_STATE_CLASS_MAP: dict[ZoneType, SensorStateClass] = {
    ZoneType.TEMPERATURE: SensorStateClass.MEASUREMENT,
    ZoneType.ANALOG_ZONE: SensorStateClass.MEASUREMENT,
}

ELK_SET_COUNTER_SERVICE_SCHEMA: VolDictType = {
    vol.Required(ATTR_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 65535))
}


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ElkM1ConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Create the Elk-M1 sensor platform."""
    elk_data = config_entry.runtime_data
    elk = elk_data.elk
    entities: list[ElkEntity] = []
    elk_settings: list[Setting] = []

    create_elk_entities(elk_data, elk.counters, "counter", ElkCounter, entities)
    create_elk_entities(elk_data, elk.keypads, "keypad", ElkKeypad, entities)
    create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities)
    create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities)

    entity_registry = er.async_get(hass)
    for setting in elk.settings:
        setting = cast(Setting, setting)
        domain = (
            "time" if setting.value_format is SettingFormat.TIME_OF_DAY else "number"
        )

        orig_unique_id = generate_unique_id(elk_data.prefix, setting)
        new_unique_id = orig_unique_id
        new_entity_id = f"{domain}.elkm1_{setting.name.replace(' ', '_')}".lower()

        if deprecate_entity(
            hass,
            entity_registry,
            "sensor",
            orig_unique_id,
            f"deprecated_sensor_{orig_unique_id}",
            "deprecated_sensor",
            new_unique_id,
            new_entity_id,
        ):
            elk_settings.append(setting)

    create_elk_entities(elk_data, elk_settings, "setting", ElkSetting, entities)
    async_add_entities(entities)

    platform = entity_platform.async_get_current_platform()

    platform.async_register_entity_service(
        SERVICE_SENSOR_COUNTER_REFRESH,
        None,
        "async_counter_refresh",
    )
    platform.async_register_entity_service(
        SERVICE_SENSOR_COUNTER_SET,
        ELK_SET_COUNTER_SERVICE_SCHEMA,
        "async_counter_set",
    )
    platform.async_register_entity_service(
        SERVICE_SENSOR_ZONE_BYPASS,
        ELK_USER_CODE_SERVICE_SCHEMA,
        "async_zone_bypass",
    )
    platform.async_register_entity_service(
        SERVICE_SENSOR_ZONE_TRIGGER,
        None,
        "async_zone_trigger",
    )


def temperature_to_state(temperature: int, undefined_temperature: int) -> str | None:
    """Convert temperature to a state."""
    return f"{temperature}" if temperature > undefined_temperature else None


class ElkSensor(ElkAttachedEntity, SensorEntity):
    """Base representation of Elk-M1 sensor."""

    _attr_native_value: str | None = None

    async def async_counter_refresh(self) -> None:
        """Refresh the value of a counter from the panel."""
        if not isinstance(self, ElkCounter):
            raise HomeAssistantError("supported only on ElkM1 Counter sensors")
        self._element.get()

    async def async_counter_set(self, value: int | None = None) -> None:
        """Set the value of a counter on the panel."""
        if not isinstance(self, ElkCounter):
            raise HomeAssistantError("supported only on ElkM1 Counter sensors")
        if value is not None:
            self._element.set(value)

    async def async_zone_bypass(self, code: int | None = None) -> None:
        """Bypass zone."""
        if not isinstance(self, ElkZone):
            raise HomeAssistantError("supported only on ElkM1 Zone sensors")
        if code is not None:
            self._element.bypass(code)

    async def async_zone_trigger(self) -> None:
        """Trigger zone."""
        if not isinstance(self, ElkZone):
            raise HomeAssistantError("supported only on ElkM1 Zone sensors")
        self._element.trigger()


class ElkCounter(ElkSensor):
    """Representation of an Elk-M1 Counter."""

    _attr_icon = "mdi:numeric"
    _element: Counter

    @override
    def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
        self._attr_native_value = self._element.value


class ElkKeypad(ElkSensor):
    """Representation of an Elk-M1 Keypad."""

    _attr_icon = "mdi:thermometer-lines"
    _element: Keypad

    @property
    def temperature_unit(self) -> str:
        """Return the temperature unit."""
        return self._temperature_unit

    @property
    @override
    def native_unit_of_measurement(self) -> str:
        """Return the unit of measurement."""
        return self._temperature_unit

    @property
    @override
    def extra_state_attributes(self) -> dict[str, Any]:
        """Attributes of the sensor."""
        attrs: dict[str, Any] = self.initial_attrs()
        attrs["area"] = self._element.area + 1
        attrs["temperature"] = self._attr_native_value
        attrs["last_user_time"] = self._element.last_user_time.isoformat()
        attrs["last_user"] = self._element.last_user + 1
        attrs["code"] = self._element.code
        attrs["last_user_name"] = self._elk.users.username(self._element.last_user)
        attrs["last_keypress"] = self._element.last_keypress
        return attrs

    @override
    def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
        self._attr_native_value = temperature_to_state(
            self._element.temperature, UNDEFINED_TEMPERATURE
        )


class ElkPanel(ElkSensor):
    """Representation of an Elk-M1 Panel."""

    _attr_translation_key = "panel"
    _attr_entity_category = EntityCategory.DIAGNOSTIC
    _element: Panel

    @property
    @override
    def extra_state_attributes(self) -> dict[str, Any]:
        """Attributes of the sensor."""
        attrs = self.initial_attrs()
        attrs["system_trouble_status"] = self._element.system_trouble_status
        return attrs

    @override
    def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
        if self._elk.is_connected():
            self._attr_native_value = "Paused" if self._elk.is_paused() else "Connected"
        else:
            self._attr_native_value = "Disconnected"


class ElkSetting(ElkSensor):
    """Representation of an Elk-M1 Setting."""

    _attr_translation_key = "setting"
    _element: Setting

    @override
    def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
        self._attr_native_value = (
            None if self._element.value is None else str(self._element.value)
        )

    @property
    @override
    def extra_state_attributes(self) -> dict[str, Any]:
        """Attributes of the sensor."""
        attrs: dict[str, Any] = self.initial_attrs()
        attrs["value_format"] = SettingFormat(self._element.value_format).name.lower()
        return attrs


class ElkZone(ElkSensor):
    """Representation of an Elk-M1 Zone."""

    _element: Zone

    @property
    @override
    def icon(self) -> str:
        """Icon to use in the frontend."""
        zone_icons = {
            ZoneType.FIRE_ALARM: "fire",
            ZoneType.FIRE_VERIFIED: "fire",
            ZoneType.FIRE_SUPERVISORY: "fire",
            ZoneType.KEYFOB: "key",
            ZoneType.NON_ALARM: "alarm-off",
            ZoneType.MEDICAL_ALARM: "medical-bag",
            ZoneType.POLICE_ALARM: "alarm-light",
            ZoneType.POLICE_NO_INDICATION: "alarm-light",
            ZoneType.KEY_MOMENTARY_ARM_DISARM: "power",
            ZoneType.KEY_MOMENTARY_ARM_AWAY: "power",
            ZoneType.KEY_MOMENTARY_ARM_STAY: "power",
            ZoneType.KEY_MOMENTARY_DISARM: "power",
            ZoneType.KEY_ON_OFF: "toggle-switch",
            ZoneType.MUTE_AUDIBLES: "volume-mute",
            ZoneType.POWER_SUPERVISORY: "power-plug",
            ZoneType.TEMPERATURE: "thermometer-lines",
            ZoneType.ANALOG_ZONE: "speedometer",
            ZoneType.PHONE_KEY: "phone-classic",
            ZoneType.INTERCOM_KEY: "deskphone",
        }
        return f"mdi:{zone_icons.get(self._element.definition, 'alarm-bell')}"

    @property
    @override
    def extra_state_attributes(self) -> dict[str, Any]:
        """Attributes of the sensor."""
        attrs: dict[str, Any] = self.initial_attrs()
        attrs["physical_status"] = self._element.physical_status.name.lower()
        attrs["logical_status"] = self._element.logical_status.name.lower()
        attrs["definition"] = self._element.definition.name.lower()
        attrs["area"] = self._element.area + 1
        attrs["triggered_alarm"] = self._element.triggered_alarm
        return attrs

    @property
    def temperature_unit(self) -> str | None:
        """Return the temperature unit."""
        if self._element.definition is ZoneType.TEMPERATURE:
            return self._temperature_unit
        return None

    @property
    @override
    def device_class(self) -> SensorDeviceClass | None:
        """Return the device class of the sensor."""
        return _DEVICE_CLASS_MAP.get(self._element.definition)

    @property
    @override
    def state_class(self) -> SensorStateClass | None:
        """Return the state class of the sensor."""
        return _STATE_CLASS_MAP.get(self._element.definition)

    @property
    @override
    def native_unit_of_measurement(self) -> str | None:
        """Return the unit of measurement."""
        if self._element.definition is ZoneType.TEMPERATURE:
            return self._temperature_unit
        if self._element.definition is ZoneType.ANALOG_ZONE:
            return UnitOfElectricPotential.VOLT
        return None

    @override
    def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
        if self._element.definition is ZoneType.TEMPERATURE:
            self._attr_native_value = temperature_to_state(
                self._element.temperature, UNDEFINED_TEMPERATURE
            )
        elif self._element.definition is ZoneType.ANALOG_ZONE:
            self._attr_native_value = f"{self._element.voltage}"
        else:
            self._attr_native_value = pretty_const(self._element.logical_status.name)
