"""Support for Tuya Cover."""
from __future__ import annotations

from dataclasses import dataclass
import logging
from typing import Any

from tuya_iot import TuyaDevice, TuyaDeviceManager

from homeassistant.components.cover import (
    ATTR_POSITION,
    ATTR_TILT_POSITION,
    SUPPORT_CLOSE,
    SUPPORT_OPEN,
    SUPPORT_SET_POSITION,
    SUPPORT_SET_TILT_POSITION,
    SUPPORT_STOP,
    CoverDeviceClass,
    CoverEntity,
    CoverEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from . import HomeAssistantTuyaData
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode

_LOGGER = logging.getLogger(__name__)


@dataclass
class TuyaCoverEntityDescription(CoverEntityDescription):
    """Describe an Tuya cover entity."""

    current_state: DPCode | None = None
    current_state_inverse: bool = False
    current_position: DPCode | tuple[DPCode, ...] | None = None
    set_position: DPCode | None = None


COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
    # Curtain
    # Note: Multiple curtains isn't documented
    # https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df
    "cl": (
        TuyaCoverEntityDescription(
            key=DPCode.CONTROL,
            name="Curtain",
            current_state=DPCode.SITUATION_SET,
            current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE),
            set_position=DPCode.PERCENT_CONTROL,
            device_class=CoverDeviceClass.CURTAIN,
        ),
        TuyaCoverEntityDescription(
            key=DPCode.CONTROL_2,
            name="Curtain 2",
            current_position=DPCode.PERCENT_STATE_2,
            set_position=DPCode.PERCENT_CONTROL_2,
            device_class=CoverDeviceClass.CURTAIN,
        ),
        TuyaCoverEntityDescription(
            key=DPCode.CONTROL_3,
            name="Curtain 3",
            current_position=DPCode.PERCENT_STATE_3,
            set_position=DPCode.PERCENT_CONTROL_3,
            device_class=CoverDeviceClass.CURTAIN,
        ),
        # switch_1 is an undocumented code that behaves identically to control
        # It is used by the Kogan Smart Blinds Driver
        TuyaCoverEntityDescription(
            key=DPCode.SWITCH_1,
            name="Blind",
            current_position=DPCode.PERCENT_CONTROL,
            set_position=DPCode.PERCENT_CONTROL,
            device_class=CoverDeviceClass.BLIND,
        ),
    ),
    # Garage Door Opener
    # https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee
    "ckmkzq": (
        TuyaCoverEntityDescription(
            key=DPCode.SWITCH_1,
            name="Door",
            current_state=DPCode.DOORCONTACT_STATE,
            current_state_inverse=True,
            device_class=CoverDeviceClass.GARAGE,
        ),
        TuyaCoverEntityDescription(
            key=DPCode.SWITCH_2,
            name="Door 2",
            current_state=DPCode.DOORCONTACT_STATE_2,
            current_state_inverse=True,
            device_class=CoverDeviceClass.GARAGE,
        ),
        TuyaCoverEntityDescription(
            key=DPCode.SWITCH_3,
            name="Door 3",
            current_state=DPCode.DOORCONTACT_STATE_3,
            current_state_inverse=True,
            device_class=CoverDeviceClass.GARAGE,
        ),
    ),
    # Curtain Switch
    # https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39
    "clkg": (
        TuyaCoverEntityDescription(
            key=DPCode.CONTROL,
            name="Curtain",
            current_position=DPCode.PERCENT_CONTROL,
            set_position=DPCode.PERCENT_CONTROL,
            device_class=CoverDeviceClass.CURTAIN,
        ),
        TuyaCoverEntityDescription(
            key=DPCode.CONTROL_2,
            name="Curtain 2",
            current_position=DPCode.PERCENT_CONTROL_2,
            set_position=DPCode.PERCENT_CONTROL_2,
            device_class=CoverDeviceClass.CURTAIN,
        ),
    ),
    # Curtain Robot
    # Note: Not documented
    "jdcljqr": (
        TuyaCoverEntityDescription(
            key=DPCode.CONTROL,
            current_position=DPCode.PERCENT_STATE,
            set_position=DPCode.PERCENT_CONTROL,
            device_class=CoverDeviceClass.CURTAIN,
        ),
    ),
}


async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities
) -> None:
    """Set up Tuya cover dynamically through Tuya discovery."""
    hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id]

    @callback
    def async_discover_device(device_ids: list[str]) -> None:
        """Discover and add a discovered tuya cover."""
        entities: list[TuyaCoverEntity] = []
        for device_id in device_ids:
            device = hass_data.device_manager.device_map[device_id]
            if descriptions := COVERS.get(device.category):
                for description in descriptions:
                    if (
                        description.key in device.function
                        or description.key in device.status
                    ):
                        entities.append(
                            TuyaCoverEntity(
                                device, hass_data.device_manager, description
                            )
                        )

        async_add_entities(entities)

    async_discover_device([*hass_data.device_manager.device_map])

    entry.async_on_unload(
        async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
    )


class TuyaCoverEntity(TuyaEntity, CoverEntity):
    """Tuya Cover Device."""

    _current_position_type: IntegerTypeData | None = None
    _set_position_type: IntegerTypeData | None = None
    _tilt_dpcode: DPCode | None = None
    _tilt_type: IntegerTypeData | None = None
    _position_dpcode: DPCode | None = None
    entity_description: TuyaCoverEntityDescription

    def __init__(
        self,
        device: TuyaDevice,
        device_manager: TuyaDeviceManager,
        description: TuyaCoverEntityDescription,
    ) -> None:
        """Init Tuya Cover."""
        super().__init__(device, device_manager)
        self.entity_description = description
        self._attr_unique_id = f"{super().unique_id}{description.key}"
        self._attr_supported_features = 0

        # Check if this cover is based on a switch or has controls
        if device.function[description.key].type == "Boolean":
            self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE
        elif device.function[description.key].type == "Enum":
            data_type = EnumTypeData.from_json(device.function[description.key].values)
            if "open" in data_type.range:
                self._attr_supported_features |= SUPPORT_OPEN
            if "close" in data_type.range:
                self._attr_supported_features |= SUPPORT_CLOSE
            if "stop" in data_type.range:
                self._attr_supported_features |= SUPPORT_STOP

        # Determine type to use for setting the position
        if (
            description.set_position is not None
            and description.set_position in device.status_range
        ):
            self._attr_supported_features |= SUPPORT_SET_POSITION
            self._set_position_type = IntegerTypeData.from_json(
                device.status_range[description.set_position].values
            )
            # Set as default, unless overwritten below
            self._current_position_type = self._set_position_type

        # Determine type for getting the position
        if (
            description.current_position is not None
            and description.current_position in device.status_range
        ):
            self._current_position_type = IntegerTypeData.from_json(
                device.status_range[description.current_position].values
            )

        # Determine type to use for setting the tilt
        if tilt_dpcode := next(
            (
                dpcode
                for dpcode in (DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL)
                if dpcode in device.function
            ),
            None,
        ):
            self._attr_supported_features |= SUPPORT_SET_TILT_POSITION
            self._tilt_dpcode = tilt_dpcode
            self._tilt_type = IntegerTypeData.from_json(
                device.status_range[tilt_dpcode].values
            )

        # Determine current_position DPCodes
        if (
            self.entity_description.current_position is None
            and self.entity_description.set_position is not None
        ):
            self._position_dpcode = self.entity_description.set_position
        elif isinstance(self.entity_description.current_position, DPCode):
            self._position_dpcode = self.entity_description.current_position
        elif isinstance(self.entity_description.current_position, tuple):
            self._position_dpcode = next(
                (
                    dpcode
                    for dpcode in self.entity_description.current_position
                    if self.device.status.get(dpcode) is not None
                ),
                None,
            )

    @property
    def current_cover_position(self) -> int | None:
        """Return cover current position."""
        if self._current_position_type is None:
            return None

        if not self._position_dpcode:
            return None

        if (position := self.device.status.get(self._position_dpcode)) is None:
            return None

        return round(
            self._current_position_type.remap_value_to(position, 0, 100, reverse=True)
        )

    @property
    def current_cover_tilt_position(self) -> int | None:
        """Return current position of cover tilt.

        None is unknown, 0 is closed, 100 is fully open.
        """
        if self._tilt_dpcode is None or self._tilt_type is None:
            return None

        if (angle := self.device.status.get(self._tilt_dpcode)) is None:
            return None

        return round(self._tilt_type.remap_value_to(angle, 0, 100))

    @property
    def is_closed(self) -> bool | None:
        """Return true if cover is closed."""
        if (
            self.entity_description.current_state is not None
            and (
                current_state := self.device.status.get(
                    self.entity_description.current_state
                )
            )
            is not None
        ):
            return self.entity_description.current_state_inverse is not (
                current_state in (True, "fully_close")
            )

        if (position := self.current_cover_position) is not None:
            return position == 0

        return None

    def open_cover(self, **kwargs: Any) -> None:
        """Open the cover."""
        value: bool | str = True
        if self.device.function[self.entity_description.key].type == "Enum":
            value = "open"

        commands: list[dict[str, str | int]] = [
            {"code": self.entity_description.key, "value": value}
        ]

        if (
            self.entity_description.set_position is not None
            and self._set_position_type is not None
        ):
            commands.append(
                {
                    "code": self.entity_description.set_position,
                    "value": round(
                        self._set_position_type.remap_value_from(
                            100, 0, 100, reverse=True
                        ),
                    ),
                }
            )

        self._send_command(commands)

    def close_cover(self, **kwargs: Any) -> None:
        """Close cover."""
        value: bool | str = False
        if self.device.function[self.entity_description.key].type == "Enum":
            value = "close"

        commands: list[dict[str, str | int]] = [
            {"code": self.entity_description.key, "value": value}
        ]

        if (
            self.entity_description.set_position is not None
            and self._set_position_type is not None
        ):
            commands.append(
                {
                    "code": self.entity_description.set_position,
                    "value": round(
                        self._set_position_type.remap_value_from(
                            0, 0, 100, reverse=True
                        ),
                    ),
                }
            )

        self._send_command(commands)

    def set_cover_position(self, **kwargs: Any) -> None:
        """Move the cover to a specific position."""
        if self._set_position_type is None:
            raise RuntimeError(
                "Cannot set position, device doesn't provide methods to set it"
            )

        self._send_command(
            [
                {
                    "code": self.entity_description.set_position,
                    "value": round(
                        self._set_position_type.remap_value_from(
                            kwargs[ATTR_POSITION], 0, 100, reverse=True
                        )
                    ),
                }
            ]
        )

    def stop_cover(self, **kwargs: Any) -> None:
        """Stop the cover."""
        self._send_command([{"code": self.entity_description.key, "value": "stop"}])

    def set_cover_tilt_position(self, **kwargs):
        """Move the cover tilt to a specific position."""
        if self._tilt_type is None:
            raise RuntimeError(
                "Cannot set tilt, device doesn't provide methods to set it"
            )

        self._send_command(
            [
                {
                    "code": self._tilt_dpcode,
                    "value": round(
                        self._tilt_type.remap_value_from(
                            kwargs[ATTR_TILT_POSITION], 0, 100, reverse=True
                        )
                    ),
                }
            ]
        )
