"""Support for the Xiaomi vacuum cleaner robot."""

from __future__ import annotations

from functools import partial
import logging
from typing import Any

from miio import DeviceException

from homeassistant.components.vacuum import (
    StateVacuumEntity,
    VacuumActivity,
    VacuumEntityFeature,
)
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import as_utc

from . import VacuumCoordinatorData
from .const import CONF_FLOW_TYPE
from .entity import XiaomiCoordinatedMiioEntity
from .typing import XiaomiMiioConfigEntry

_LOGGER = logging.getLogger(__name__)

ATTR_ERROR = "error"
ATTR_STATUS = "status"
ATTR_TIMERS = "timers"

STATE_CODE_TO_STATE = {
    1: VacuumActivity.IDLE,  # "Starting"
    2: VacuumActivity.IDLE,  # "Charger disconnected"
    3: VacuumActivity.IDLE,  # "Idle"
    4: VacuumActivity.CLEANING,  # "Remote control active"
    5: VacuumActivity.CLEANING,  # "Cleaning"
    6: VacuumActivity.RETURNING,  # "Returning home"
    7: VacuumActivity.CLEANING,  # "Manual mode"
    8: VacuumActivity.DOCKED,  # "Charging"
    9: VacuumActivity.ERROR,  # "Charging problem"
    10: VacuumActivity.PAUSED,  # "Paused"
    11: VacuumActivity.CLEANING,  # "Spot cleaning"
    12: VacuumActivity.ERROR,  # "Error"
    13: VacuumActivity.IDLE,  # "Shutting down"
    14: VacuumActivity.DOCKED,  # "Updating"
    15: VacuumActivity.RETURNING,  # "Docking"
    16: VacuumActivity.CLEANING,  # "Going to target"
    17: VacuumActivity.CLEANING,  # "Zoned cleaning"
    18: VacuumActivity.CLEANING,  # "Segment cleaning"
    22: VacuumActivity.DOCKED,  # "Emptying the bin" on s7+
    23: VacuumActivity.DOCKED,  # "Washing the mop" on s7maxV
    26: VacuumActivity.RETURNING,  # "Going to wash the mop" on s7maxV
    100: VacuumActivity.DOCKED,  # "Charging complete"
    101: VacuumActivity.ERROR,  # "Device offline"
}


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: XiaomiMiioConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the Xiaomi vacuum cleaner robot from a config entry."""
    entities = []

    if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
        unique_id = config_entry.unique_id

        mirobo = MiroboVacuum(
            config_entry.runtime_data.device,
            config_entry,
            unique_id,
            config_entry.runtime_data.device_coordinator,
        )
        entities.append(mirobo)

    async_add_entities(entities, update_before_add=True)


class MiroboVacuum(
    XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[VacuumCoordinatorData]],
    StateVacuumEntity,
):
    """Representation of a Xiaomi Vacuum cleaner robot."""

    _attr_name = None
    _attr_supported_features = (
        VacuumEntityFeature.STATE
        | VacuumEntityFeature.PAUSE
        | VacuumEntityFeature.STOP
        | VacuumEntityFeature.RETURN_HOME
        | VacuumEntityFeature.FAN_SPEED
        | VacuumEntityFeature.SEND_COMMAND
        | VacuumEntityFeature.LOCATE
        | VacuumEntityFeature.BATTERY
        | VacuumEntityFeature.CLEAN_SPOT
        | VacuumEntityFeature.START
    )

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

    @property
    def activity(self) -> VacuumActivity | None:
        """Return the status of the vacuum cleaner."""
        # The vacuum reverts back to an idle state after erroring out.
        # We want to keep returning an error until it has been cleared.
        if self.coordinator.data.status.got_error:
            return VacuumActivity.ERROR

        return super().activity

    @property
    def battery_level(self) -> int:
        """Return the battery level of the vacuum cleaner."""
        return self.coordinator.data.status.battery

    @property
    def fan_speed(self) -> str:
        """Return the fan speed of the vacuum cleaner."""
        speed = self.coordinator.data.status.fanspeed
        if speed in self.coordinator.data.fan_speeds_reverse:
            return self.coordinator.data.fan_speeds_reverse[speed]

        _LOGGER.debug("Unable to find reverse for %s", speed)

        return str(speed)

    @property
    def fan_speed_list(self) -> list[str]:
        """Get the list of available fan speed steps of the vacuum cleaner."""
        if speed_list := self.coordinator.data.fan_speeds:
            return list(speed_list)
        return []

    @property
    def timers(self) -> list[dict[str, Any]]:
        """Get the list of added timers of the vacuum cleaner."""
        return [
            {
                "enabled": timer.enabled,
                "cron": timer.cron,
                "next_schedule": as_utc(timer.next_schedule),
            }
            for timer in self.coordinator.data.timers
        ]

    @property
    def extra_state_attributes(self) -> dict[str, Any]:
        """Return the specific state attributes of this vacuum cleaner."""
        attrs: dict[str, Any] = {}
        attrs[ATTR_STATUS] = str(self.coordinator.data.status.state)

        if self.coordinator.data.status.got_error:
            attrs[ATTR_ERROR] = self.coordinator.data.status.error

        if self.timers:
            attrs[ATTR_TIMERS] = self.timers
        return attrs

    async def _try_command(self, mask_error, func, *args, **kwargs):
        """Call a vacuum command handling error messages."""
        try:
            await self.hass.async_add_executor_job(partial(func, *args, **kwargs))
            await self.coordinator.async_refresh()
        except DeviceException as exc:
            _LOGGER.error(mask_error, exc)
            return False
        return True

    async def async_start(self) -> None:
        """Start or resume the cleaning task."""
        await self._try_command(
            "Unable to start the vacuum: %s",
            self._device.resume_or_start,  # type: ignore[attr-defined]
        )

    async def async_pause(self) -> None:
        """Pause the cleaning task."""
        await self._try_command(
            "Unable to set start/pause: %s",
            self._device.pause,  # type: ignore[attr-defined]
        )

    async def async_stop(self, **kwargs: Any) -> None:
        """Stop the vacuum cleaner."""
        await self._try_command(
            "Unable to stop: %s",
            self._device.stop,  # type: ignore[attr-defined]
        )

    async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
        """Set fan speed."""
        if fan_speed in self.coordinator.data.fan_speeds:
            fan_speed_int = self.coordinator.data.fan_speeds[fan_speed]
        else:
            try:
                fan_speed_int = int(fan_speed)
            except ValueError as exc:
                _LOGGER.error(
                    "Fan speed step not recognized (%s). Valid speeds are: %s",
                    exc,
                    self.fan_speed_list,
                )
                return
        await self._try_command(
            "Unable to set fan speed: %s",
            self._device.set_fan_speed,  # type: ignore[attr-defined]
            fan_speed_int,
        )

    async def async_return_to_base(self, **kwargs: Any) -> None:
        """Set the vacuum cleaner to return to the dock."""
        await self._try_command(
            "Unable to return home: %s",
            self._device.home,  # type: ignore[attr-defined]
        )

    async def async_clean_spot(self, **kwargs: Any) -> None:
        """Perform a spot clean-up."""
        await self._try_command(
            "Unable to start the vacuum for a spot clean-up: %s",
            self._device.spot,  # type: ignore[attr-defined]
        )

    async def async_locate(self, **kwargs: Any) -> None:
        """Locate the vacuum cleaner."""
        await self._try_command(
            "Unable to locate the botvac: %s",
            self._device.find,  # type: ignore[attr-defined]
        )

    async def async_send_command(
        self,
        command: str,
        params: dict[str, Any] | list[Any] | None = None,
        **kwargs: Any,
    ) -> None:
        """Send raw command."""
        await self._try_command(
            "Unable to send command to the vacuum: %s",
            self._device.raw_command,
            command,
            params,
        )

    async def async_remote_control_start(self) -> None:
        """Start remote control mode."""
        await self._try_command(
            "Unable to start remote control the vacuum: %s",
            self._device.manual_start,  # type: ignore[attr-defined]
        )

    async def async_remote_control_stop(self) -> None:
        """Stop remote control mode."""
        await self._try_command(
            "Unable to stop remote control the vacuum: %s",
            self._device.manual_stop,  # type: ignore[attr-defined]
        )

    async def async_remote_control_move(
        self, rotation: int = 0, velocity: float = 0.3, duration: int = 1500
    ) -> None:
        """Move vacuum with remote control mode."""
        await self._try_command(
            "Unable to move with remote control the vacuum: %s",
            self._device.manual_control,  # type: ignore[attr-defined]
            velocity=velocity,
            rotation=rotation,
            duration=duration,
        )

    async def async_remote_control_move_step(
        self, rotation: int = 0, velocity: float = 0.2, duration: int = 1500
    ) -> None:
        """Move vacuum one step with remote control mode."""
        await self._try_command(
            "Unable to remote control the vacuum: %s",
            self._device.manual_control_once,  # type: ignore[attr-defined]
            velocity=velocity,
            rotation=rotation,
            duration=duration,
        )

    async def async_goto(self, x_coord: int, y_coord: int) -> None:
        """Goto the specified coordinates."""
        await self._try_command(
            "Unable to send the vacuum cleaner to the specified coordinates: %s",
            self._device.goto,  # type: ignore[attr-defined]
            x_coord=x_coord,
            y_coord=y_coord,
        )

    async def async_clean_segment(self, segments) -> None:
        """Clean the specified segments(s)."""
        if isinstance(segments, int):
            segments = [segments]

        await self._try_command(
            "Unable to start cleaning of the specified segments: %s",
            self._device.segment_clean,  # type: ignore[attr-defined]
            segments=segments,
        )

    async def async_clean_zone(self, zone: list[Any], repeats: int = 1) -> None:
        """Clean selected area for the number of repeats indicated."""
        for _zone in zone:
            _zone.append(repeats)
        _LOGGER.debug("Zone with repeats: %s", zone)
        try:
            await self.hass.async_add_executor_job(
                self._device.zoned_clean,  # type: ignore[attr-defined]
                zone,
            )
            await self.coordinator.async_refresh()
        except (OSError, DeviceException) as exc:
            _LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc)

    @callback
    def _handle_coordinator_update(self) -> None:
        state_code = int(self.coordinator.data.status.state_code)
        if state_code not in STATE_CODE_TO_STATE:
            _LOGGER.error(
                "STATE not supported: %s, state_code: %s",
                self.coordinator.data.status.state,
                self.coordinator.data.status.state_code,
            )
            self._attr_activity = None
        else:
            self._attr_activity = STATE_CODE_TO_STATE[state_code]

        super()._handle_coordinator_update()
