"""Switch for Shelly."""

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

from aioshelly.block_device import Block
from aioshelly.const import RPC_GENERATIONS
from aioshelly.exceptions import RpcCallError

from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.switch import (
    DOMAIN as SWITCH_DOMAIN,
    SwitchEntity,
    SwitchEntityDescription,
)
from homeassistant.const import STATE_ON, EntityCategory
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.restore_state import RestoreEntity

from .const import (
    DOMAIN,
    MODEL_FRANKEVER_IRRIGATION_CONTROLLER,
    MODEL_LINKEDGO_ST802_THERMOSTAT,
    MODEL_LINKEDGO_ST1820_THERMOSTAT,
    MODEL_NEO_WATER_VALVE,
    MODEL_TOP_EV_CHARGER_EVE01,
    ROLE_GENERIC,
    RPC_ERROR_CODE_REMOTE_DISABLED,
)
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
    BlockEntityDescription,
    RpcEntityDescription,
    ShellyBlockAttributeEntity,
    ShellyRpcAttributeEntity,
    ShellySleepingBlockAttributeEntity,
    async_setup_entry_block,
    async_setup_entry_rpc,
    rpc_call,
)
from .utils import (
    async_remove_orphaned_entities,
    get_device_entry_gen,
    get_rpc_channel_name,
    get_virtual_component_ids,
    is_block_exclude_from_relay,
    is_rpc_exclude_from_relay,
    is_view_for_platform,
)

PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription):
    """Class to describe a BLOCK switch."""


BLOCK_RELAY_SWITCHES = {
    ("relay", "output"): BlockSwitchDescription(
        key="relay|output",
        removal_condition=is_block_exclude_from_relay,
    )
}

BLOCK_SLEEPING_MOTION_SWITCH = {
    ("sensor", "motionActive"): BlockSwitchDescription(
        key="sensor|motionActive",
        translation_key="motion_detection",
        entity_category=EntityCategory.CONFIG,
    )
}


@dataclass(frozen=True, kw_only=True)
class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription):
    """Class to describe a RPC virtual switch."""

    is_on: Callable[[dict[str, Any]], bool]
    method_on: str
    method_off: str
    method_params_fn: Callable[[int | None, bool], tuple]


class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity):
    """Entity that controls a switch on RPC based Shelly devices."""

    entity_description: RpcSwitchDescription

    def __init__(
        self,
        coordinator: ShellyRpcCoordinator,
        key: str,
        attribute: str,
        description: RpcSwitchDescription,
    ) -> None:
        """Initialize switch."""
        super().__init__(coordinator, key, attribute, description)

        if description.key in ("cb", "switch", "script"):
            self._attr_name = get_rpc_channel_name(coordinator.device, key)

    @property
    @override
    def is_on(self) -> bool:
        """If switch is on."""
        return self.entity_description.is_on(self.status)

    @rpc_call
    @override
    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on switch."""
        method = getattr(self.coordinator.device, self.entity_description.method_on)

        if TYPE_CHECKING:
            assert method is not None

        params = self.entity_description.method_params_fn(self._id, True)
        await method(*params)

    @rpc_call
    @override
    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off switch."""
        method = getattr(self.coordinator.device, self.entity_description.method_off)

        if TYPE_CHECKING:
            assert method is not None

        params = self.entity_description.method_params_fn(self._id, False)
        await method(*params)


class RpcCircuitBreakerSwitch(RpcSwitch):
    """Entity that controls a circuit breaker on RPC based Shelly devices."""

    @property
    @override
    def available(self) -> bool:
        """Return if entity is available."""
        return super().available and not self.status["safety"]

    @rpc_call
    @override
    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on circuit breaker."""
        method = getattr(self.coordinator.device, self.entity_description.method_on)

        if TYPE_CHECKING:
            assert method is not None

        params = self.entity_description.method_params_fn(self._id, True)
        try:
            await method(*params)
        except RpcCallError as err:
            if err.code == RPC_ERROR_CODE_REMOTE_DISABLED:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="circuit_breaker_remote_disabled",
                    translation_placeholders={
                        "entity": self.entity_id,
                        "device": self.coordinator.name,
                    },
                ) from err
            raise


class RpcRelaySwitch(RpcSwitch):
    """Entity that controls a switch on RPC based Shelly devices."""

    def __init__(
        self,
        coordinator: ShellyRpcCoordinator,
        key: str,
        attribute: str,
        description: RpcSwitchDescription,
    ) -> None:
        """Initialize the switch."""
        super().__init__(coordinator, key, attribute, description)
        self._attr_unique_id: str = f"{coordinator.mac}-{key}"


RPC_SWITCHES = {
    "switch": RpcSwitchDescription(
        key="switch",
        sub_key="output",
        removal_condition=is_rpc_exclude_from_relay,
        is_on=lambda status: bool(status["output"]),
        method_on="switch_set",
        method_off="switch_set",
        method_params_fn=lambda id, value: (id, value),
        entity_class=RpcRelaySwitch,
    ),
    "cb": RpcSwitchDescription(
        key="cb",
        sub_key="output",
        is_on=lambda status: bool(status["output"]),
        method_on="cb_set",
        method_off="cb_set",
        method_params_fn=lambda id, value: (id, value),
        entity_class=RpcCircuitBreakerSwitch,
    ),
    "boolean_generic": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        removal_condition=lambda config, _, key: (
            not is_view_for_platform(config, key, SWITCH_DOMAIN)
        ),
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role=ROLE_GENERIC,
    ),
    "boolean_anti_freeze": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="frost_protection",
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="anti_freeze",
        models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
    ),
    "boolean_child_lock": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="child_lock",
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="child_lock",
        models={MODEL_LINKEDGO_ST1820_THERMOSTAT},
    ),
    "boolean_enable": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="thermostat_enabled",
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="enable",
        models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
    ),
    "boolean_start_charging": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="charging",
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="start_charging",
        models={MODEL_TOP_EV_CHARGER_EVE01},
    ),
    "boolean_state": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="valve_opened",
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="state",
        models={MODEL_NEO_WATER_VALVE},
    ),
    "boolean_zone0": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="zone_with_number",
        translation_placeholders={"zone_number": "1"},
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="zone0",
        models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
    ),
    "boolean_zone1": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="zone_with_number",
        translation_placeholders={"zone_number": "2"},
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="zone1",
        models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
    ),
    "boolean_zone2": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="zone_with_number",
        translation_placeholders={"zone_number": "3"},
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="zone2",
        models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
    ),
    "boolean_zone3": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="zone_with_number",
        translation_placeholders={"zone_number": "4"},
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="zone3",
        models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
    ),
    "boolean_zone4": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="zone_with_number",
        translation_placeholders={"zone_number": "5"},
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="zone4",
        models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
    ),
    "boolean_zone5": RpcSwitchDescription(
        key="boolean",
        sub_key="value",
        translation_key="zone_with_number",
        translation_placeholders={"zone_number": "6"},
        entity_registry_enabled_default=False,
        is_on=lambda status: bool(status["value"]),
        method_on="boolean_set",
        method_off="boolean_set",
        method_params_fn=lambda id, value: (id, value),
        role="zone5",
        models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
    ),
    "script": RpcSwitchDescription(
        key="script",
        sub_key="running",
        is_on=lambda status: bool(status["running"]),
        method_on="script_start",
        method_off="script_stop",
        method_params_fn=lambda id, _: (id,),
        entity_registry_enabled_default=False,
        entity_category=EntityCategory.CONFIG,
    ),
    "cury_left": RpcSwitchDescription(
        key="cury",
        sub_key="slots",
        translation_key="left_slot",
        is_on=lambda status: bool(status["slots"]["left"]["on"]),
        method_on="cury_set",
        method_off="cury_set",
        method_params_fn=lambda id, value: (id, "left", value),
        available=lambda status: (
            (left := status["left"]) is not None
            and left.get("vial", {}).get("level", -1) != -1
        ),
    ),
    "cury_left_boost": RpcSwitchDescription(
        key="cury",
        sub_key="slots",
        translation_key="left_slot_boost",
        is_on=lambda status: status["slots"]["left"]["boost"] is not None,
        method_on="cury_boost",
        method_off="cury_stop_boost",
        method_params_fn=lambda id, _: (id, "left"),
        available=lambda status: (
            (left := status["left"]) is not None
            and left.get("vial", {}).get("level", -1) != -1
        ),
    ),
    "cury_right": RpcSwitchDescription(
        key="cury",
        sub_key="slots",
        translation_key="right_slot",
        is_on=lambda status: bool(status["slots"]["right"]["on"]),
        method_on="cury_set",
        method_off="cury_set",
        method_params_fn=lambda id, value: (id, "right", value),
        available=lambda status: (
            (right := status["right"]) is not None
            and right.get("vial", {}).get("level", -1) != -1
        ),
    ),
    "cury_right_boost": RpcSwitchDescription(
        key="cury",
        sub_key="slots",
        translation_key="right_slot_boost",
        is_on=lambda status: status["slots"]["right"]["boost"] is not None,
        method_on="cury_boost",
        method_off="cury_stop_boost",
        method_params_fn=lambda id, _: (id, "right"),
        available=lambda status: (
            (right := status["right"]) is not None
            and right.get("vial", {}).get("level", -1) != -1
        ),
    ),
    "cury_away_mode": RpcSwitchDescription(
        key="cury",
        sub_key="away_mode",
        translation_key="cury_away_mode",
        is_on=lambda status: status["away_mode"],
        method_on="cury_set_away_mode",
        method_off="cury_set_away_mode",
        method_params_fn=lambda id, value: (id, value),
    ),
}


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ShellyConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up switch entities."""
    if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
        return _async_setup_rpc_entry(hass, config_entry, async_add_entities)

    return _async_setup_block_entry(hass, config_entry, async_add_entities)


@callback
def _async_setup_block_entry(
    hass: HomeAssistant,
    config_entry: ShellyConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up entities for BLOCK device."""
    coordinator = config_entry.runtime_data.block
    assert coordinator

    async_setup_entry_block(
        hass, config_entry, async_add_entities, BLOCK_RELAY_SWITCHES, BlockRelaySwitch
    )

    async_setup_entry_block(
        hass,
        config_entry,
        async_add_entities,
        BLOCK_SLEEPING_MOTION_SWITCH,
        BlockSleepingMotionSwitch,
    )


@callback
def _async_setup_rpc_entry(
    hass: HomeAssistant,
    config_entry: ShellyConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up entities for RPC device."""
    coordinator = config_entry.runtime_data.rpc
    assert coordinator

    async_setup_entry_rpc(
        hass, config_entry, async_add_entities, RPC_SWITCHES, RpcSwitch
    )

    # the user can remove virtual components from the device configuration, so we need
    # to remove orphaned entities
    virtual_switch_ids = get_virtual_component_ids(
        coordinator.device.config, SWITCH_DOMAIN
    )
    async_remove_orphaned_entities(
        hass,
        config_entry.entry_id,
        coordinator.mac,
        SWITCH_DOMAIN,
        virtual_switch_ids,
        "boolean",
    )

    # if the script is removed, from the device configuration, we need
    # to remove orphaned entities
    async_remove_orphaned_entities(
        hass,
        config_entry.entry_id,
        coordinator.mac,
        SWITCH_DOMAIN,
        coordinator.device.status,
        "script",
    )

    # if the climate is removed, from the device configuration, we need
    # to remove orphaned entities
    async_remove_orphaned_entities(
        hass,
        config_entry.entry_id,
        coordinator.mac,
        CLIMATE_DOMAIN,
        coordinator.device.status,
        "thermostat",
    )


class BlockSleepingMotionSwitch(
    ShellySleepingBlockAttributeEntity, RestoreEntity, SwitchEntity
):
    """Entity that controls Motion Sensor on Block based Shelly devices."""

    entity_description: BlockSwitchDescription

    def __init__(
        self,
        coordinator: ShellyBlockCoordinator,
        block: Block | None,
        attribute: str,
        description: BlockSwitchDescription,
        entry: RegistryEntry | None = None,
    ) -> None:
        """Initialize the sleeping sensor."""
        super().__init__(coordinator, block, attribute, description, entry)
        self.last_state: State | None = None

    @property
    @override
    def is_on(self) -> bool | None:
        """If motion is active."""
        if self.block is not None:
            return bool(self.block.motionActive)

        if self.last_state is None:
            return None

        return self.last_state.state == STATE_ON

    @override
    async def async_turn_on(self, **kwargs: Any) -> None:
        """Activate switch."""
        await self.coordinator.device.set_shelly_motion_detection(True)
        self.async_write_ha_state()

    @override
    async def async_turn_off(self, **kwargs: Any) -> None:
        """Deactivate switch."""
        await self.coordinator.device.set_shelly_motion_detection(False)
        self.async_write_ha_state()

    @override
    async def async_added_to_hass(self) -> None:
        """Handle entity which will be added."""
        await super().async_added_to_hass()
        if (last_state := await self.async_get_last_state()) is not None:
            self.last_state = last_state


class BlockRelaySwitch(ShellyBlockAttributeEntity, SwitchEntity):
    """Entity that controls a relay on Block based Shelly devices."""

    entity_description: BlockSwitchDescription

    def __init__(
        self,
        coordinator: ShellyBlockCoordinator,
        block: Block,
        attribute: str,
        description: BlockSwitchDescription,
    ) -> None:
        """Initialize relay switch."""
        super().__init__(coordinator, block, attribute, description)
        self.control_result: dict[str, Any] | None = None
        self._attr_name = None  # Main device entity
        self._attr_unique_id: str = f"{coordinator.mac}-{block.description}"

    @property
    @override
    def is_on(self) -> bool:
        """If switch is on."""
        if self.control_result:
            return cast(bool, self.control_result["ison"])

        return bool(self.block.output)

    @override
    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on relay."""
        self.control_result = await self.set_state(turn="on")
        self.async_write_ha_state()

    @override
    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off relay."""
        self.control_result = await self.set_state(turn="off")
        self.async_write_ha_state()

    @callback
    @override
    def _update_callback(self) -> None:
        """When device updates, clear control result that overrides state."""
        self.control_result = None
        super()._update_callback()
