"""Component providing support for Reolink select entities."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any

from reolink_aio.api import (
    BinningModeEnum,
    Chime,
    ChimeToneEnum,
    DayNightEnum,
    EncodingEnum,
    ExposureEnum,
    HDREnum,
    Host,
    HubToneEnum,
    SpotlightEventModeEnum,
    SpotlightModeEnum,
    StatusLedEnum,
    TrackMethodEnum,
)

from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory, UnitOfDataRate, UnitOfFrequency
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .entity import (
    ReolinkChannelCoordinatorEntity,
    ReolinkChannelEntityDescription,
    ReolinkChimeCoordinatorEntity,
    ReolinkChimeEntityDescription,
    ReolinkHostChimeCoordinatorEntity,
    ReolinkHostCoordinatorEntity,
    ReolinkHostEntityDescription,
)
from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error

_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class ReolinkSelectEntityDescription(
    SelectEntityDescription,
    ReolinkChannelEntityDescription,
):
    """A class that describes select entities."""

    get_options: list[str] | Callable[[Host, int], list[str]]
    method: Callable[[Host, int, str], Any]
    value: Callable[[Host, int], str] | None = None


@dataclass(frozen=True, kw_only=True)
class ReolinkHostSelectEntityDescription(
    SelectEntityDescription,
    ReolinkHostEntityDescription,
):
    """A class that describes host select entities."""

    get_options: Callable[[Host], list[str]]
    method: Callable[[Host, str], Any]
    value: Callable[[Host], str]


@dataclass(frozen=True, kw_only=True)
class ReolinkChimeSelectEntityDescription(
    SelectEntityDescription,
    ReolinkChimeEntityDescription,
):
    """A class that describes select entities for a chime."""

    get_options: list[str]
    method: Callable[[Chime, str], Any]
    value: Callable[[Chime], str | None]


def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int:
    """Get the quick reply file id from the message string."""
    return [k for k, v in api.quick_reply_dict(ch).items() if v == mess][0]


SELECT_ENTITIES = (
    ReolinkSelectEntityDescription(
        key="floodlight_mode",
        cmd_key="GetWhiteLed",
        cmd_id=[289, 438],
        translation_key="floodlight_mode",
        entity_category=EntityCategory.CONFIG,
        get_options=lambda api, ch: api.whiteled_mode_list(ch),
        supported=lambda api, ch: api.supported(ch, "floodLight"),
        value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name,
        method=lambda api, ch, name: api.set_whiteled(ch, mode=name),
    ),
    ReolinkSelectEntityDescription(
        key="floodlight_event_mode",
        cmd_key="GetWhiteLed",
        cmd_id=[289, 438],
        translation_key="floodlight_event_mode",
        entity_category=EntityCategory.CONFIG,
        get_options=[mode.name for mode in SpotlightEventModeEnum],
        supported=lambda api, ch: api.supported(ch, "floodlight_event"),
        value=lambda api, ch: SpotlightEventModeEnum(api.whiteled_event_mode(ch)).name,
        method=lambda api, ch, name: api.baichuan.set_floodlight(
            ch, event_mode=SpotlightEventModeEnum[name].value
        ),
    ),
    ReolinkSelectEntityDescription(
        key="day_night_mode",
        cmd_key="GetIsp",
        cmd_id=26,
        translation_key="day_night_mode",
        entity_category=EntityCategory.CONFIG,
        get_options=[mode.name for mode in DayNightEnum],
        supported=lambda api, ch: api.supported(ch, "dayNight"),
        value=lambda api, ch: DayNightEnum(api.daynight_state(ch)).name,
        method=lambda api, ch, name: api.set_daynight(ch, DayNightEnum[name].value),
    ),
    ReolinkSelectEntityDescription(
        key="ptz_preset",
        translation_key="ptz_preset",
        get_options=lambda api, ch: list(api.ptz_presets(ch)),
        supported=lambda api, ch: api.supported(ch, "ptz_presets"),
        method=lambda api, ch, name: api.set_ptz_command(ch, preset=name),
    ),
    ReolinkSelectEntityDescription(
        key="play_quick_reply_message",
        translation_key="play_quick_reply_message",
        get_options=lambda api, ch: list(api.quick_reply_dict(ch).values())[1:],
        supported=lambda api, ch: api.supported(ch, "play_quick_reply"),
        method=lambda api, ch, mess: api.play_quick_reply(
            ch, file_id=_get_quick_reply_id(api, ch, mess)
        ),
    ),
    ReolinkSelectEntityDescription(
        key="auto_quick_reply_message",
        cmd_key="GetAutoReply",
        translation_key="auto_quick_reply_message",
        entity_category=EntityCategory.CONFIG,
        get_options=lambda api, ch: list(api.quick_reply_dict(ch).values()),
        supported=lambda api, ch: api.supported(ch, "quick_reply"),
        value=lambda api, ch: api.quick_reply_dict(ch)[api.quick_reply_file(ch)],
        method=lambda api, ch, mess: api.set_quick_reply(
            ch, file_id=_get_quick_reply_id(api, ch, mess)
        ),
    ),
    ReolinkSelectEntityDescription(
        key="hub_alarm_ringtone",
        cmd_key="GetDeviceAudioCfg",
        translation_key="hub_alarm_ringtone",
        entity_category=EntityCategory.CONFIG,
        get_options=[mode.name for mode in HubToneEnum],
        supported=lambda api, ch: api.supported(ch, "hub_audio"),
        value=lambda api, ch: HubToneEnum(api.hub_alarm_tone_id(ch)).name,
        method=lambda api, ch, name: api.set_hub_audio(
            ch, alarm_tone_id=HubToneEnum[name].value
        ),
    ),
    ReolinkSelectEntityDescription(
        key="hub_visitor_ringtone",
        cmd_key="GetDeviceAudioCfg",
        translation_key="hub_visitor_ringtone",
        entity_category=EntityCategory.CONFIG,
        get_options=[mode.name for mode in HubToneEnum],
        supported=lambda api, ch: (
            api.supported(ch, "hub_audio") and api.is_doorbell(ch)
        ),
        value=lambda api, ch: HubToneEnum(api.hub_visitor_tone_id(ch)).name,
        method=lambda api, ch, name: api.set_hub_audio(
            ch, visitor_tone_id=HubToneEnum[name].value
        ),
    ),
    ReolinkSelectEntityDescription(
        key="auto_track_method",
        cmd_key="GetAiCfg",
        translation_key="auto_track_method",
        entity_category=EntityCategory.CONFIG,
        get_options=[method.name for method in TrackMethodEnum],
        supported=lambda api, ch: api.supported(ch, "auto_track_method"),
        value=lambda api, ch: TrackMethodEnum(api.auto_track_method(ch)).name,
        method=lambda api, ch, name: api.set_auto_tracking(ch, method=name),
    ),
    ReolinkSelectEntityDescription(
        key="status_led",
        cmd_key="GetPowerLed",
        translation_key="doorbell_led",
        entity_category=EntityCategory.CONFIG,
        get_options=lambda api, ch: api.doorbell_led_list(ch),
        supported=lambda api, ch: api.supported(ch, "doorbell_led"),
        value=lambda api, ch: StatusLedEnum(api.doorbell_led(ch)).name,
        method=lambda api, ch, name: api.set_status_led(
            ch, StatusLedEnum[name].value, doorbell=True
        ),
    ),
    ReolinkSelectEntityDescription(
        key="hdr",
        cmd_key="GetIsp",
        translation_key="hdr",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=[method.name for method in HDREnum],
        supported=lambda api, ch: api.supported(ch, "HDR"),
        value=lambda api, ch: HDREnum(api.HDR_state(ch)).name,
        method=lambda api, ch, name: api.set_HDR(ch, HDREnum[name].value),
    ),
    ReolinkSelectEntityDescription(
        key="exposure",
        cmd_key="GetIsp",
        translation_key="exposure",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=[method.name for method in ExposureEnum],
        supported=lambda api, ch: api.supported(ch, "exposure"),
        value=lambda api, ch: ExposureEnum(api.exposure(ch)).name,
        method=lambda api, ch, name: api.set_exposure(ch, ExposureEnum[name].value),
    ),
    ReolinkSelectEntityDescription(
        key="binning_mode",
        cmd_key="GetIsp",
        translation_key="binning_mode",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=[method.name for method in BinningModeEnum],
        supported=lambda api, ch: api.supported(ch, "binning_mode"),
        value=lambda api, ch: BinningModeEnum(api.binning_mode(ch)).name,
        method=lambda api, ch, name: api.set_binning_mode(
            ch, BinningModeEnum[name].value
        ),
    ),
    ReolinkSelectEntityDescription(
        key="main_frame_rate",
        cmd_key="GetEnc",
        translation_key="main_frame_rate",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        unit_of_measurement=UnitOfFrequency.HERTZ,
        get_options=lambda api, ch: [str(v) for v in api.frame_rate_list(ch, "main")],
        supported=lambda api, ch: api.supported(ch, "frame_rate"),
        value=lambda api, ch: str(api.frame_rate(ch, "main")),
        method=lambda api, ch, value: api.set_frame_rate(ch, int(value), "main"),
    ),
    ReolinkSelectEntityDescription(
        key="sub_frame_rate",
        cmd_key="GetEnc",
        translation_key="sub_frame_rate",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        unit_of_measurement=UnitOfFrequency.HERTZ,
        get_options=lambda api, ch: [str(v) for v in api.frame_rate_list(ch, "sub")],
        supported=lambda api, ch: api.supported(ch, "frame_rate"),
        value=lambda api, ch: str(api.frame_rate(ch, "sub")),
        method=lambda api, ch, value: api.set_frame_rate(ch, int(value), "sub"),
    ),
    ReolinkSelectEntityDescription(
        key="main_bit_rate",
        cmd_key="GetEnc",
        translation_key="main_bit_rate",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
        get_options=lambda api, ch: [str(v) for v in api.bit_rate_list(ch, "main")],
        supported=lambda api, ch: api.supported(ch, "bit_rate"),
        value=lambda api, ch: str(api.bit_rate(ch, "main")),
        method=lambda api, ch, value: api.set_bit_rate(ch, int(value), "main"),
    ),
    ReolinkSelectEntityDescription(
        key="sub_bit_rate",
        cmd_key="GetEnc",
        translation_key="sub_bit_rate",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
        get_options=lambda api, ch: [str(v) for v in api.bit_rate_list(ch, "sub")],
        supported=lambda api, ch: api.supported(ch, "bit_rate"),
        value=lambda api, ch: str(api.bit_rate(ch, "sub")),
        method=lambda api, ch, value: api.set_bit_rate(ch, int(value), "sub"),
    ),
    ReolinkSelectEntityDescription(
        key="main_encoding",
        cmd_key="GetEnc",
        translation_key="main_encoding",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=[val.name for val in EncodingEnum],
        supported=lambda api, ch: api.supported(ch, "encoding"),
        value=lambda api, ch: api.encoding(ch, "main"),
        method=lambda api, ch, value: api.set_encoding(ch, value, "main"),
    ),
    ReolinkSelectEntityDescription(
        key="sub_encoding",
        cmd_key="GetEnc",
        translation_key="sub_encoding",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=[val.name for val in EncodingEnum],
        supported=lambda api, ch: api.supported(ch, "encoding"),
        value=lambda api, ch: api.encoding(ch, "sub"),
        method=lambda api, ch, value: api.set_encoding(ch, value, "sub"),
    ),
    ReolinkSelectEntityDescription(
        key="pre_record_fps",
        cmd_key="594",
        translation_key="pre_record_fps",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        unit_of_measurement=UnitOfFrequency.HERTZ,
        get_options=["1", "2", "5"],
        supported=lambda api, ch: api.supported(ch, "pre_record"),
        value=lambda api, ch: str(api.baichuan.pre_record_fps(ch)),
        method=lambda api, ch, value: api.baichuan.set_pre_recording(
            ch, fps=int(value)
        ),
    ),
    ReolinkSelectEntityDescription(
        key="post_rec_time",
        cmd_key="GetRec",
        translation_key="post_rec_time",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=lambda api, ch: api.post_recording_time_list(ch),
        supported=lambda api, ch: api.supported(ch, "post_rec_time"),
        value=lambda api, ch: api.post_recording_time(ch),
        method=lambda api, ch, value: api.set_post_recording_time(ch, value),
    ),
)

HOST_SELECT_ENTITIES = (
    ReolinkHostSelectEntityDescription(
        key="scene_mode",
        cmd_key="GetScene",
        translation_key="scene_mode",
        entity_category=EntityCategory.CONFIG,
        get_options=lambda api: api.baichuan.scene_names,
        supported=lambda api: api.supported(None, "scenes"),
        value=lambda api: api.baichuan.active_scene,
        method=lambda api, name: api.baichuan.set_scene(scene_name=name),
    ),
    ReolinkHostSelectEntityDescription(
        key="packing_time",
        cmd_key="GetRec",
        translation_key="packing_time",
        entity_category=EntityCategory.CONFIG,
        entity_registry_enabled_default=False,
        get_options=lambda api: api.recording_packing_time_list,
        supported=lambda api: api.supported(None, "pak_time"),
        value=lambda api: api.recording_packing_time,
        method=lambda api, value: api.set_recording_packing_time(value),
    ),
)

CHIME_SELECT_ENTITIES = (
    ReolinkChimeSelectEntityDescription(
        key="motion_tone",
        cmd_key="GetDingDongCfg",
        translation_key="motion_tone",
        entity_category=EntityCategory.CONFIG,
        supported=lambda chime: "md" in chime.chime_event_types,
        get_options=[method.name for method in ChimeToneEnum],
        value=lambda chime: chime.tone_name("md"),
        method=lambda chime, name: chime.set_tone("md", ChimeToneEnum[name].value),
    ),
    ReolinkChimeSelectEntityDescription(
        key="people_tone",
        cmd_key="GetDingDongCfg",
        translation_key="people_tone",
        entity_category=EntityCategory.CONFIG,
        get_options=[method.name for method in ChimeToneEnum],
        supported=lambda chime: "people" in chime.chime_event_types,
        value=lambda chime: chime.tone_name("people"),
        method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value),
    ),
    ReolinkChimeSelectEntityDescription(
        key="vehicle_tone",
        cmd_key="GetDingDongCfg",
        translation_key="vehicle_tone",
        entity_category=EntityCategory.CONFIG,
        get_options=[method.name for method in ChimeToneEnum],
        supported=lambda chime: "vehicle" in chime.chime_event_types,
        value=lambda chime: chime.tone_name("vehicle"),
        method=lambda chime, name: chime.set_tone("vehicle", ChimeToneEnum[name].value),
    ),
    ReolinkChimeSelectEntityDescription(
        key="visitor_tone",
        cmd_key="GetDingDongCfg",
        translation_key="visitor_tone",
        entity_category=EntityCategory.CONFIG,
        get_options=[method.name for method in ChimeToneEnum],
        supported=lambda chime: "visitor" in chime.chime_event_types,
        value=lambda chime: chime.tone_name("visitor"),
        method=lambda chime, name: chime.set_tone("visitor", ChimeToneEnum[name].value),
    ),
    ReolinkChimeSelectEntityDescription(
        key="package_tone",
        cmd_key="GetDingDongCfg",
        translation_key="package_tone",
        entity_category=EntityCategory.CONFIG,
        get_options=[method.name for method in ChimeToneEnum],
        supported=lambda chime: "package" in chime.chime_event_types,
        value=lambda chime: chime.tone_name("package"),
        method=lambda chime, name: chime.set_tone("package", ChimeToneEnum[name].value),
    ),
    ReolinkChimeSelectEntityDescription(
        key="pet_tone",
        cmd_key="GetDingDongCfg",
        translation_key="pet_tone",
        entity_category=EntityCategory.CONFIG,
        get_options=[method.name for method in ChimeToneEnum],
        supported=lambda chime: "dog_cat" in chime.chime_event_types,
        value=lambda chime: chime.tone_name("dog_cat"),
        method=lambda chime, name: chime.set_tone("dog_cat", ChimeToneEnum[name].value),
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ReolinkConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up a Reolink select entities."""
    reolink_data: ReolinkData = config_entry.runtime_data

    entities: list[SelectEntity] = [
        ReolinkSelectEntity(reolink_data, channel, entity_description)
        for entity_description in SELECT_ENTITIES
        for channel in reolink_data.host.api.channels
        if entity_description.supported(reolink_data.host.api, channel)
    ]
    entities.extend(
        ReolinkHostSelectEntity(reolink_data, entity_description)
        for entity_description in HOST_SELECT_ENTITIES
        if entity_description.supported(reolink_data.host.api)
    )
    entities.extend(
        ReolinkChimeSelectEntity(reolink_data, chime, entity_description)
        for entity_description in CHIME_SELECT_ENTITIES
        for chime in reolink_data.host.api.chime_list
        if entity_description.supported(chime) and chime.channel is not None
    )
    entities.extend(
        ReolinkHostChimeSelectEntity(reolink_data, chime, entity_description)
        for entity_description in CHIME_SELECT_ENTITIES
        for chime in reolink_data.host.api.chime_list
        if entity_description.supported(chime) and chime.channel is None
    )
    async_add_entities(entities)


class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
    """Base select entity class for Reolink IP cameras."""

    entity_description: ReolinkSelectEntityDescription

    def __init__(
        self,
        reolink_data: ReolinkData,
        channel: int,
        entity_description: ReolinkSelectEntityDescription,
    ) -> None:
        """Initialize Reolink select entity."""
        self.entity_description = entity_description
        super().__init__(reolink_data, channel)
        self._log_error = True

        if callable(entity_description.get_options):
            self._attr_options = entity_description.get_options(self._host.api, channel)
        else:
            self._attr_options = entity_description.get_options

    @property
    def current_option(self) -> str | None:
        """Return the current option."""
        if self.entity_description.value is None:
            return None

        try:
            option = self.entity_description.value(self._host.api, self._channel)
        except ValueError, KeyError:
            if self._log_error:
                _LOGGER.exception("Reolink '%s' has an unknown value", self.name)
                self._log_error = False
            return None

        self._log_error = True
        return option

    @raise_translated_error
    async def async_select_option(self, option: str) -> None:
        """Change the selected option."""
        await self.entity_description.method(self._host.api, self._channel, option)
        self.async_write_ha_state()


class ReolinkHostSelectEntity(ReolinkHostCoordinatorEntity, SelectEntity):
    """Base select entity class for Reolink Host."""

    entity_description: ReolinkHostSelectEntityDescription

    def __init__(
        self,
        reolink_data: ReolinkData,
        entity_description: ReolinkHostSelectEntityDescription,
    ) -> None:
        """Initialize Reolink select entity."""
        self.entity_description = entity_description
        super().__init__(reolink_data)
        self._attr_options = entity_description.get_options(self._host.api)

    @property
    def current_option(self) -> str | None:
        """Return the current option."""
        return self.entity_description.value(self._host.api)

    @raise_translated_error
    async def async_select_option(self, option: str) -> None:
        """Change the selected option."""
        await self.entity_description.method(self._host.api, option)
        self.async_write_ha_state()


class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
    """Base select entity class for Reolink chimes connected through a camera."""

    entity_description: ReolinkChimeSelectEntityDescription

    def __init__(
        self,
        reolink_data: ReolinkData,
        chime: Chime,
        entity_description: ReolinkChimeSelectEntityDescription,
    ) -> None:
        """Initialize Reolink select entity for a chime."""
        self.entity_description = entity_description
        super().__init__(reolink_data, chime)
        self._attr_options = entity_description.get_options

    @property
    def current_option(self) -> str | None:
        """Return the current option."""
        return self.entity_description.value(self._chime)

    @raise_translated_error
    async def async_select_option(self, option: str) -> None:
        """Change the selected option."""
        await self.entity_description.method(self._chime, option)
        self.async_write_ha_state()


class ReolinkHostChimeSelectEntity(ReolinkHostChimeCoordinatorEntity, SelectEntity):
    """Base select entity class for Reolink chimes connected to a host."""

    entity_description: ReolinkChimeSelectEntityDescription

    def __init__(
        self,
        reolink_data: ReolinkData,
        chime: Chime,
        entity_description: ReolinkChimeSelectEntityDescription,
    ) -> None:
        """Initialize Reolink select entity for a chime."""
        self.entity_description = entity_description
        super().__init__(reolink_data, chime)
        self._attr_options = entity_description.get_options

    @property
    def current_option(self) -> str | None:
        """Return the current option."""
        return self.entity_description.value(self._chime)

    @raise_translated_error
    async def async_select_option(self, option: str) -> None:
        """Change the selected option."""
        await self.entity_description.method(self._chime, option)
        self.async_write_ha_state()
