"""Matter vacuum platform."""

from dataclasses import dataclass
from enum import IntEnum
import logging
from typing import TYPE_CHECKING, Any, override

from chip.clusters import Objects as clusters
from matter_server.client.models import device_types

from homeassistant.components.vacuum import (
    Segment,
    StateVacuumEntity,
    StateVacuumEntityDescription,
    VacuumActivity,
    VacuumEntityFeature,
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .entity import MatterEntity, MatterEntityDescription
from .helpers import MatterConfigEntry
from .models import MatterDiscoverySchema

_LOGGER = logging.getLogger(__name__)


class OperationalState(IntEnum):
    """Operational State of the vacuum cleaner.

    Combination of generic OperationalState and RvcOperationalState.
    """

    STOPPED = 0x00
    RUNNING = 0x01
    PAUSED = 0x02
    ERROR = 0x03
    SEEKING_CHARGER = 0x40
    CHARGING = 0x41
    DOCKED = 0x42


class ModeTag(IntEnum):
    """Enum with available ModeTag values."""

    IDLE = 0x4000  # 16384 decimal
    CLEANING = 0x4001  # 16385 decimal
    MAPPING = 0x4002  # 16386 decimal


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: MatterConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up Matter vacuum platform from Config Entry."""
    matter = config_entry.runtime_data.adapter
    matter.register_platform_handler(Platform.VACUUM, async_add_entities)


@dataclass(frozen=True, kw_only=True)
class MatterStateVacuumEntityDescription(
    StateVacuumEntityDescription, MatterEntityDescription
):
    """Describe Matter Vacuum entities."""


class MatterVacuum(MatterEntity, StateVacuumEntity):
    """Representation of a Matter Vacuum cleaner entity."""

    _last_accepted_commands: list[int] | None = None
    _last_service_area_feature_map: int | None = None
    _supported_run_modes: (
        dict[int, clusters.RvcRunMode.Structs.ModeOptionStruct] | None
    ) = None
    entity_description: MatterStateVacuumEntityDescription
    _platform_translation_key = "vacuum"

    def _get_run_mode_by_tag(
        self, tag: ModeTag
    ) -> clusters.RvcRunMode.Structs.ModeOptionStruct | None:
        """Get the run mode by tag."""
        supported_run_modes = self._supported_run_modes or {}
        for mode in supported_run_modes.values():
            for t in mode.modeTags:
                if t.value == tag.value:
                    return mode
        return None

    @override
    async def async_stop(self, **kwargs: Any) -> None:
        """Stop the vacuum cleaner."""
        # We simply set the RvcRunMode to the first runmode
        # that has the idle tag to stop the vacuum cleaner.
        # this is compatible with both Matter 1.2 and 1.3+ devices.
        mode = self._get_run_mode_by_tag(ModeTag.IDLE)
        if mode is None:
            raise HomeAssistantError(
                "No supported run mode found to stop the vacuum cleaner."
            )

        await self.send_device_command(
            clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
        )

    @override
    async def async_return_to_base(self, **kwargs: Any) -> None:
        """Set the vacuum cleaner to return to the dock."""
        await self.send_device_command(clusters.RvcOperationalState.Commands.GoHome())

    @override
    async def async_locate(self, **kwargs: Any) -> None:
        """Locate the vacuum cleaner."""
        await self.send_device_command(clusters.Identify.Commands.Identify())

    @override
    async def async_start(self) -> None:
        """Start or resume the cleaning task."""
        if TYPE_CHECKING:
            assert self._last_accepted_commands is not None

        accepted_operational_commands = self._last_accepted_commands
        if (
            clusters.RvcOperationalState.Commands.Resume.command_id
            in accepted_operational_commands
            and self.state == VacuumActivity.PAUSED
        ):
            # vacuum is paused and supports resume command
            await self.send_device_command(
                clusters.RvcOperationalState.Commands.Resume()
            )
            return

        # We simply set the RvcRunMode to the first runmode
        # that has the cleaning tag to start the vacuum cleaner.
        # this is compatible with both Matter 1.2 and 1.3+ devices.
        mode = self._get_run_mode_by_tag(ModeTag.CLEANING)
        if mode is None:
            raise HomeAssistantError(
                "No supported run mode found to start the vacuum cleaner."
            )

        # Reset selected areas to an unconstrained selection to ensure start
        # performs a full clean and does not reuse a previous area-targeted
        # selection.
        if VacuumEntityFeature.CLEAN_AREA in self.supported_features:
            # Matter ServiceArea: an empty NewAreas list means unconstrained
            # operation (full clean).
            await self.send_device_command(
                clusters.ServiceArea.Commands.SelectAreas(newAreas=[])
            )

        await self.send_device_command(
            clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
        )

    @override
    async def async_pause(self) -> None:
        """Pause the cleaning task."""
        await self.send_device_command(clusters.RvcOperationalState.Commands.Pause())

    @property
    def _current_segments(self) -> dict[str, Segment]:
        """Return the current cleanable segments reported by the device."""
        supported_areas: list[clusters.ServiceArea.Structs.AreaStruct] = (
            self.get_matter_attribute_value(
                clusters.ServiceArea.Attributes.SupportedAreas
            )
        )

        segments: dict[str, Segment] = {}
        for area in supported_areas:
            area_name = None
            location_info = area.areaInfo.locationInfo
            if location_info not in (None, clusters.NullValue):
                area_name = location_info.locationName

            if area_name:
                segment_id = str(area.areaID)
                segments[segment_id] = Segment(id=segment_id, name=area_name)

        return segments

    @override
    async def async_get_segments(self) -> list[Segment]:
        """Get the segments that can be cleaned.

        Returns a list of segments containing their ids and names.
        """
        return list(self._current_segments.values())

    @override
    async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
        """Clean the specified segments.

        Args:
            segment_ids: List of segment IDs to clean.
            **kwargs: Additional arguments (unused).

        """
        area_ids = [int(segment_id) for segment_id in segment_ids]

        mode = self._get_run_mode_by_tag(ModeTag.CLEANING)
        if mode is None:
            raise HomeAssistantError(
                "No supported run mode found to start the vacuum cleaner."
            )

        response = await self.send_device_command(
            clusters.ServiceArea.Commands.SelectAreas(newAreas=area_ids)
        )

        if (
            response
            and response["status"]
            != clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess
        ):
            raise HomeAssistantError(
                "Failed to select areas: "
                f"{response['statusText'] or response['status']}"
            )

        await self.send_device_command(
            clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
        )

    @callback
    @override
    def _update_from_device(self) -> None:
        """Update from device."""
        self._calculate_features()
        # derive state from the run mode + operational state
        run_mode_raw: int = self.get_matter_attribute_value(
            clusters.RvcRunMode.Attributes.CurrentMode
        )
        operational_state: int = self.get_matter_attribute_value(
            clusters.RvcOperationalState.Attributes.OperationalState
        )
        state: VacuumActivity | None = None
        if TYPE_CHECKING:
            assert self._supported_run_modes is not None
        if operational_state in (OperationalState.CHARGING, OperationalState.DOCKED):
            state = VacuumActivity.DOCKED
        elif operational_state == OperationalState.SEEKING_CHARGER:
            state = VacuumActivity.RETURNING
        elif operational_state == OperationalState.ERROR:
            state = VacuumActivity.ERROR
        elif operational_state == OperationalState.PAUSED:
            state = VacuumActivity.PAUSED
        elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
            tags = {x.value for x in run_mode.modeTags}
            if ModeTag.CLEANING in tags:
                state = VacuumActivity.CLEANING
            elif ModeTag.IDLE in tags:
                state = VacuumActivity.IDLE
            elif ModeTag.MAPPING in tags:
                state = VacuumActivity.CLEANING
        self._attr_activity = state

        if (
            VacuumEntityFeature.CLEAN_AREA in self.supported_features
            and self.registry_entry is not None
            and (last_seen_segments := self.last_seen_segments) is not None
            # Ignore empty segments; some devices transiently
            # report an empty list before sending the real one.
            and (current_segments := self._current_segments)
        ):
            last_seen_by_id = {s.id: s for s in last_seen_segments}
            if current_segments != last_seen_by_id:
                _LOGGER.debug(
                    "Vacuum segments changed: last_seen=%s, current=%s",
                    last_seen_by_id,
                    current_segments,
                )
                self.async_create_segments_issue()

    @callback
    def _calculate_features(self) -> None:
        """Calculate features for HA Vacuum platform."""
        accepted_operational_commands: list[int] = self.get_matter_attribute_value(
            clusters.RvcOperationalState.Attributes.AcceptedCommandList
        )
        service_area_feature_map: int | None = self.get_matter_attribute_value(
            clusters.ServiceArea.Attributes.FeatureMap
        )

        # In principle the feature set should not change, except for accepted
        # commands and service area feature map.
        if (
            self._last_accepted_commands == accepted_operational_commands
            and self._last_service_area_feature_map == service_area_feature_map
        ):
            return

        self._last_accepted_commands = accepted_operational_commands
        self._last_service_area_feature_map = service_area_feature_map
        supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
        supported_features |= VacuumEntityFeature.START
        supported_features |= VacuumEntityFeature.STATE
        supported_features |= VacuumEntityFeature.STOP

        # optional identify cluster = locate feature (value must be not None or 0)
        if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
            supported_features |= VacuumEntityFeature.LOCATE
        # create a map of supported run modes
        run_modes: list[clusters.RvcRunMode.Structs.ModeOptionStruct] = (
            self.get_matter_attribute_value(
                clusters.RvcRunMode.Attributes.SupportedModes
            )
        )
        self._supported_run_modes = {mode.mode: mode for mode in run_modes}
        # map operational state commands to vacuum features
        if (
            clusters.RvcOperationalState.Commands.Pause.command_id
            in accepted_operational_commands
        ):
            supported_features |= VacuumEntityFeature.PAUSE
        if (
            clusters.RvcOperationalState.Commands.GoHome.command_id
            in accepted_operational_commands
        ):
            supported_features |= VacuumEntityFeature.RETURN_HOME
        # Check if Map feature is enabled for clean area support
        if (
            service_area_feature_map is not None
            and service_area_feature_map & clusters.ServiceArea.Bitmaps.Feature.kMaps
        ):
            supported_features |= VacuumEntityFeature.CLEAN_AREA

        self._attr_supported_features = supported_features


# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
    MatterDiscoverySchema(
        platform=Platform.VACUUM,
        entity_description=MatterStateVacuumEntityDescription(
            key="MatterVacuumCleaner", name=None
        ),
        entity_class=MatterVacuum,
        required_attributes=(
            clusters.RvcRunMode.Attributes.CurrentMode,
            clusters.RvcOperationalState.Attributes.OperationalState,
        ),
        optional_attributes=(
            clusters.ServiceArea.Attributes.FeatureMap,
            clusters.ServiceArea.Attributes.SupportedAreas,
        ),
        device_type=(device_types.RoboticVacuumCleaner,),
        allow_none_value=True,
    ),
]
