"""Support for esphome selects."""

from __future__ import annotations

from dataclasses import replace

from aioesphomeapi import EntityInfo, SelectInfo, SelectState

from homeassistant.components.assist_pipeline import (
    AssistPipelineSelect,
    VadSensitivitySelect,
)
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import restore_state
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN, NO_WAKE_WORD
from .entity import (
    EsphomeAssistEntity,
    EsphomeEntity,
    convert_api_error_ha_error,
    esphome_state_property,
    platform_async_setup_entry,
)
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData

PARALLEL_UPDATES = 0


async def async_setup_entry(
    hass: HomeAssistant,
    entry: ESPHomeConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up esphome selects based on a config entry."""
    await platform_async_setup_entry(
        hass,
        entry,
        async_add_entities,
        info_type=SelectInfo,
        entity_type=EsphomeSelect,
        state_type=SelectState,
    )

    entry_data = entry.runtime_data
    assert entry_data.device_info is not None
    if entry_data.device_info.voice_assistant_feature_flags_compat(
        entry_data.api_version
    ):
        async_add_entities(
            [
                EsphomeAssistPipelineSelect(hass, entry_data, index=0),
                EsphomeAssistPipelineSelect(hass, entry_data, index=1),
                EsphomeVadSensitivitySelect(hass, entry_data),
                EsphomeAssistSatelliteWakeWordSelect(entry_data, index=0),
                EsphomeAssistSatelliteWakeWordSelect(entry_data, index=1),
            ]
        )


class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity):
    """A select implementation for esphome."""

    @callback
    def _on_static_info_update(self, static_info: EntityInfo) -> None:
        """Set attrs from static info."""
        super()._on_static_info_update(static_info)
        self._attr_options = self._static_info.options

    @property
    @esphome_state_property
    def current_option(self) -> str | None:
        """Return the state of the entity."""
        state = self._state
        return None if state.missing_state else state.state

    @convert_api_error_ha_error
    async def async_select_option(self, option: str) -> None:
        """Change the selected option."""
        self._client.select_command(
            self._key, option, device_id=self._static_info.device_id
        )


class EsphomeAssistPipelineSelect(EsphomeAssistEntity, AssistPipelineSelect):
    """Pipeline selector for esphome devices."""

    def __init__(
        self, hass: HomeAssistant, entry_data: RuntimeEntryData, index: int = 0
    ) -> None:
        """Initialize a pipeline selector."""
        EsphomeAssistEntity.__init__(self, entry_data)
        AssistPipelineSelect.__init__(
            self, hass, DOMAIN, self._device_info.mac_address, index=index
        )


class EsphomeVadSensitivitySelect(EsphomeAssistEntity, VadSensitivitySelect):
    """VAD sensitivity selector for ESPHome devices."""

    def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
        """Initialize a VAD sensitivity selector."""
        EsphomeAssistEntity.__init__(self, entry_data)
        VadSensitivitySelect.__init__(self, hass, self._device_info.mac_address)


class EsphomeAssistSatelliteWakeWordSelect(
    EsphomeAssistEntity, SelectEntity, restore_state.RestoreEntity
):
    """Wake word selector for esphome devices."""

    entity_description = SelectEntityDescription(
        key="wake_word",
        translation_key="wake_word",
        entity_category=EntityCategory.CONFIG,
    )

    _attr_current_option: str | None = None
    _attr_options: list[str] = [NO_WAKE_WORD]

    def __init__(self, entry_data: RuntimeEntryData, index: int = 0) -> None:
        """Initialize a wake word selector."""
        if index < 1:
            # Keep compatibility
            key_suffix = ""
            placeholder = ""
        else:
            key_suffix = f"_{index + 1}"
            placeholder = f" {index + 1}"

        self.entity_description = replace(
            self.entity_description,
            key=f"wake_word{key_suffix}",
            translation_placeholders={"index": placeholder},
        )

        EsphomeAssistEntity.__init__(self, entry_data)

        unique_id_prefix = self._device_info.mac_address
        self._attr_unique_id = f"{unique_id_prefix}-{self.entity_description.key}"

        # name -> id
        self._wake_words: dict[str, str] = {}
        self._wake_word_index = index

    @property
    def available(self) -> bool:
        """Return if entity is available."""
        return len(self._attr_options) > 1  # more than just NO_WAKE_WORD

    async def async_added_to_hass(self) -> None:
        """Run when entity about to be added to hass."""
        await super().async_added_to_hass()

        if last_state := await self.async_get_last_state():
            self._attr_current_option = last_state.state

        # Update options when config is updated
        self.async_on_remove(
            self._entry_data.async_register_assist_satellite_config_updated_callback(
                self.async_satellite_config_updated
            )
        )

    async def async_select_option(self, option: str) -> None:
        """Select an option."""
        self._attr_current_option = option
        self.async_write_ha_state()

        wake_word_id = self._wake_words.get(option)
        self._entry_data.async_assist_satellite_set_wake_word(
            self._wake_word_index, wake_word_id
        )

    def async_satellite_config_updated(
        self, config: AssistSatelliteConfiguration
    ) -> None:
        """Update options with available wake words."""
        if (not config.available_wake_words) or (config.max_active_wake_words < 1):
            # No wake words
            self._wake_words.clear()
            self._attr_current_option = NO_WAKE_WORD
            self._attr_options = [NO_WAKE_WORD]
            self._entry_data.assist_satellite_wake_words.pop(
                self._wake_word_index, None
            )
            self.async_write_ha_state()
            return

        self._wake_words = {w.wake_word: w.id for w in config.available_wake_words}
        self._attr_options = [NO_WAKE_WORD, *sorted(self._wake_words)]

        option = self._attr_current_option

        if (
            (self._wake_word_index == 0)
            and (len(config.active_wake_words) == 1)
            and (option in (None, NO_WAKE_WORD))
        ):
            option = next(
                (
                    wake_word
                    for wake_word, wake_word_id in self._wake_words.items()
                    if wake_word_id == config.active_wake_words[0]
                ),
                None,
            )

        if (
            (option is None)
            or ((wake_word_id := self._wake_words.get(option)) is None)
            or (wake_word_id not in config.active_wake_words)
        ):
            option = NO_WAKE_WORD

        self._attr_current_option = option
        self.async_write_ha_state()

        # Keep entry data in sync
        if wake_word_id := self._wake_words.get(option):
            self._entry_data.assist_satellite_wake_words[self._wake_word_index] = (
                wake_word_id
            )
        else:
            self._entry_data.assist_satellite_wake_words.pop(
                self._wake_word_index, None
            )
