"""Intents for the media_player integration."""

import asyncio
from collections.abc import Iterable
from dataclasses import dataclass, field
import logging
import time
from typing import cast

import voluptuous as vol

from homeassistant.const import (
    SERVICE_MEDIA_NEXT_TRACK,
    SERVICE_MEDIA_PAUSE,
    SERVICE_MEDIA_PLAY,
    SERVICE_MEDIA_PREVIOUS_TRACK,
    SERVICE_VOLUME_MUTE,
    SERVICE_VOLUME_SET,
    STATE_PLAYING,
)
from homeassistant.core import Context, HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.helpers.entity_component import EntityComponent

from . import MediaPlayerDeviceClass, MediaPlayerEntity
from .browse_media import SearchMedia
from .const import (
    ATTR_MEDIA_FILTER_CLASSES,
    ATTR_MEDIA_VOLUME_LEVEL,
    ATTR_MEDIA_VOLUME_MUTED,
    DOMAIN,
    INTENT_MEDIA_NEXT,
    INTENT_MEDIA_PAUSE,
    INTENT_MEDIA_PREVIOUS,
    INTENT_MEDIA_SEARCH_AND_PLAY,
    INTENT_MEDIA_UNPAUSE,
    INTENT_PLAYER_MUTE,
    INTENT_PLAYER_UNMUTE,
    INTENT_SET_VOLUME,
    INTENT_SET_VOLUME_RELATIVE,
    SERVICE_PLAY_MEDIA,
    SERVICE_SEARCH_MEDIA,
    MediaClass,
    MediaPlayerEntityFeature,
    MediaPlayerState,
)

_LOGGER = logging.getLogger(__name__)


@dataclass
class LastPaused:
    """Information about last media players that were paused by voice."""

    timestamp: float | None = None
    context: Context | None = None
    entity_ids: set[str] = field(default_factory=set)

    def clear(self) -> None:
        """Clear timestamp and entities."""
        self.timestamp = None
        self.context = None
        self.entity_ids.clear()

    def update(self, context: Context | None, entity_ids: Iterable[str]) -> None:
        """Update last paused group."""
        self.context = context
        self.entity_ids = set(entity_ids)
        if self.entity_ids:
            self.timestamp = time.time()

    def __bool__(self) -> bool:
        """Return True if timestamp is set."""
        return self.timestamp is not None


async def async_setup_intents(hass: HomeAssistant) -> None:
    """Set up the media_player intents."""
    last_paused = LastPaused()

    intent.async_register(hass, MediaUnpauseHandler(last_paused))
    intent.async_register(hass, MediaPauseHandler(last_paused))
    intent.async_register(
        hass,
        intent.ServiceIntentHandler(
            INTENT_MEDIA_NEXT,
            DOMAIN,
            SERVICE_MEDIA_NEXT_TRACK,
            required_domains={DOMAIN},
            required_features=MediaPlayerEntityFeature.NEXT_TRACK,
            required_states={MediaPlayerState.PLAYING},
            description="Skips a media player to the next item",
            platforms={DOMAIN},
            device_classes={MediaPlayerDeviceClass},
        ),
    )
    intent.async_register(
        hass,
        intent.ServiceIntentHandler(
            INTENT_MEDIA_PREVIOUS,
            DOMAIN,
            SERVICE_MEDIA_PREVIOUS_TRACK,
            required_domains={DOMAIN},
            required_features=MediaPlayerEntityFeature.PREVIOUS_TRACK,
            required_states={MediaPlayerState.PLAYING},
            description="Replays the previous item for a media player",
            platforms={DOMAIN},
            device_classes={MediaPlayerDeviceClass},
        ),
    )
    intent.async_register(
        hass,
        intent.ServiceIntentHandler(
            INTENT_SET_VOLUME,
            DOMAIN,
            SERVICE_VOLUME_SET,
            required_domains={DOMAIN},
            required_features=MediaPlayerEntityFeature.VOLUME_SET,
            required_slots={
                ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo(
                    description="The volume percentage of the media player",
                    value_schema=vol.All(
                        vol.Coerce(int),
                        vol.Range(min=0, max=100),
                        lambda val: val / 100,
                    ),
                ),
            },
            description="Sets the volume percentage of a media player",
            platforms={DOMAIN},
            device_classes={MediaPlayerDeviceClass},
        ),
    )
    intent.async_register(hass, MediaSetVolumeRelativeHandler())
    intent.async_register(hass, MediaPlayerMuteUnmuteHandler(True))
    intent.async_register(hass, MediaPlayerMuteUnmuteHandler(False))
    intent.async_register(hass, MediaSearchAndPlayHandler())


class MediaPauseHandler(intent.ServiceIntentHandler):
    """Handler for pause intent. Records last paused media players."""

    def __init__(self, last_paused: LastPaused) -> None:
        """Initialize handler."""
        super().__init__(
            INTENT_MEDIA_PAUSE,
            DOMAIN,
            SERVICE_MEDIA_PAUSE,
            required_domains={DOMAIN},
            required_features=MediaPlayerEntityFeature.PAUSE,
            required_states={MediaPlayerState.PLAYING},
            description="Pauses a media player",
            platforms={DOMAIN},
            device_classes={MediaPlayerDeviceClass},
        )
        self.last_paused = last_paused

    async def async_handle_states(
        self,
        intent_obj: intent.Intent,
        match_result: intent.MatchTargetsResult,
        match_constraints: intent.MatchTargetsConstraints,
        match_preferences: intent.MatchTargetsPreferences | None = None,
    ) -> intent.IntentResponse:
        """Record last paused media players."""
        if match_result.is_match:
            # Save entity ids of paused media players
            self.last_paused.update(
                intent_obj.context, (s.entity_id for s in match_result.states)
            )

        return await super().async_handle_states(
            intent_obj, match_result, match_constraints
        )


class MediaUnpauseHandler(intent.ServiceIntentHandler):
    """Handler for unpause/resume intent. Uses last paused media players."""

    def __init__(self, last_paused: LastPaused) -> None:
        """Initialize handler."""
        super().__init__(
            INTENT_MEDIA_UNPAUSE,
            DOMAIN,
            SERVICE_MEDIA_PLAY,
            required_domains={DOMAIN},
            description="Resumes a media player",
            platforms={DOMAIN},
            device_classes={MediaPlayerDeviceClass},
        )
        self.last_paused = last_paused

    async def async_handle_states(
        self,
        intent_obj: intent.Intent,
        match_result: intent.MatchTargetsResult,
        match_constraints: intent.MatchTargetsConstraints,
        match_preferences: intent.MatchTargetsPreferences | None = None,
    ) -> intent.IntentResponse:
        """Unpause last paused media players."""
        if match_result.is_match and (not match_constraints.name) and self.last_paused:
            assert self.last_paused.timestamp is not None

            # Check for a media player that was paused more recently than the
            # ones by voice.
            recent_state: State | None = None
            for state in match_result.states:
                if (state.last_changed_timestamp <= self.last_paused.timestamp) or (
                    state.context == self.last_paused.context
                ):
                    continue

                if (recent_state is None) or (
                    state.last_changed_timestamp > recent_state.last_changed_timestamp
                ):
                    recent_state = state

            if recent_state is not None:
                # Resume the more recently paused media player (outside of voice).
                match_result.states = [recent_state]
            else:
                # Resume only the previously paused media players if they are in the
                # targeted set.
                targeted_ids = {s.entity_id for s in match_result.states}
                overlapping_ids = targeted_ids.intersection(self.last_paused.entity_ids)
                if overlapping_ids:
                    match_result.states = [
                        s for s in match_result.states if s.entity_id in overlapping_ids
                    ]

            self.last_paused.clear()

        return await super().async_handle_states(
            intent_obj, match_result, match_constraints
        )


class MediaPlayerMuteUnmuteHandler(intent.ServiceIntentHandler):
    """Handle Mute/Unmute intents."""

    def __init__(self, is_volume_muted: bool) -> None:
        """Initialize the mute/unmute handler objects."""

        super().__init__(
            (INTENT_PLAYER_MUTE if is_volume_muted else INTENT_PLAYER_UNMUTE),
            DOMAIN,
            SERVICE_VOLUME_MUTE,
            required_domains={DOMAIN},
            required_features=MediaPlayerEntityFeature.VOLUME_MUTE,
            optional_slots={
                ATTR_MEDIA_VOLUME_MUTED: intent.IntentSlotInfo(
                    description="Whether the media player should be muted or unmuted",
                    value_schema=vol.Boolean(),
                ),
            },
            description=(
                "Mutes a media player" if is_volume_muted else "Unmutes a media player"
            ),
            platforms={DOMAIN},
            device_classes={MediaPlayerDeviceClass},
        )
        self.is_volume_muted = is_volume_muted

    async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
        """Handle the intent."""

        intent_obj.slots["is_volume_muted"] = {
            "value": self.is_volume_muted,
            "text": str(self.is_volume_muted),
        }
        return await super().async_handle(intent_obj)


class MediaSearchAndPlayHandler(intent.IntentHandler):
    """Handle HassMediaSearchAndPlay intents."""

    description = "Searches for media and plays the first result"

    intent_type = INTENT_MEDIA_SEARCH_AND_PLAY
    slot_schema = {
        vol.Required("search_query"): cv.string,
        vol.Optional("media_class"): vol.In([cls.value for cls in MediaClass]),
        # Optional name/area/floor slots handled by intent matcher
        vol.Optional("name"): cv.string,
        vol.Optional("area"): cv.string,
        vol.Optional("floor"): cv.string,
        vol.Optional("preferred_area_id"): cv.string,
        vol.Optional("preferred_floor_id"): cv.string,
    }
    platforms = {DOMAIN}

    async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
        """Handle the intent."""
        hass = intent_obj.hass
        slots = self.async_validate_slots(intent_obj.slots)
        search_query = slots["search_query"]["value"]

        # Entity name to match
        name_slot = slots.get("name", {})
        entity_name: str | None = name_slot.get("value")

        # Get area/floor info
        area_slot = slots.get("area", {})
        area_id = area_slot.get("value")

        floor_slot = slots.get("floor", {})
        floor_id = floor_slot.get("value")

        # Find matching entities
        match_constraints = intent.MatchTargetsConstraints(
            name=entity_name,
            area_name=area_id,
            floor_name=floor_id,
            domains={DOMAIN},
            assistant=intent_obj.assistant,
            features=MediaPlayerEntityFeature.SEARCH_MEDIA
            | MediaPlayerEntityFeature.PLAY_MEDIA,
            single_target=True,
        )
        match_result = intent.async_match_targets(
            hass,
            match_constraints,
            intent.MatchTargetsPreferences(
                area_id=slots.get("preferred_area_id", {}).get("value"),
                floor_id=slots.get("preferred_floor_id", {}).get("value"),
            ),
        )

        if not match_result.is_match:
            raise intent.MatchFailedError(
                result=match_result, constraints=match_constraints
            )

        target_entity = match_result.states[0]
        target_entity_id = target_entity.entity_id

        # Get media class if provided
        media_class_slot = slots.get("media_class", {})
        media_class_value = media_class_slot.get("value")

        # Build search service data
        search_data = {"search_query": search_query}

        # Add media_filter_classes if media_class is provided
        if media_class_value:
            search_data[ATTR_MEDIA_FILTER_CLASSES] = [media_class_value]

        # 1. Search Media
        try:
            search_response = await hass.services.async_call(
                DOMAIN,
                SERVICE_SEARCH_MEDIA,
                search_data,
                target={
                    "entity_id": target_entity_id,
                },
                blocking=True,
                context=intent_obj.context,
                return_response=True,
            )
        except HomeAssistantError as err:
            _LOGGER.error("Error calling search_media: %s", err)
            raise intent.IntentHandleError(f"Error searching media: {err}") from err

        if (
            not search_response
            or not (
                entity_response := cast(
                    SearchMedia, search_response.get(target_entity_id)
                )
            )
            or not (results := entity_response.result)
        ):
            # No results found
            return intent_obj.create_response()

        # 2. Play Media (first result)
        first_result = results[0]
        try:
            await hass.services.async_call(
                DOMAIN,
                SERVICE_PLAY_MEDIA,
                {
                    "entity_id": target_entity_id,
                    "media_content_id": first_result.media_content_id,
                    "media_content_type": first_result.media_content_type,
                },
                blocking=True,
                context=intent_obj.context,
            )
        except HomeAssistantError as err:
            _LOGGER.error("Error calling play_media: %s", err)
            raise intent.IntentHandleError(f"Error playing media: {err}") from err

        # Success
        response = intent_obj.create_response()
        response.async_set_speech_slots({"media": first_result.as_dict()})
        return response


class MediaSetVolumeRelativeHandler(intent.IntentHandler):
    """Handler for setting relative volume."""

    description = "Increases or decreases the volume of a media player"

    intent_type = INTENT_SET_VOLUME_RELATIVE
    slot_schema = {
        vol.Required("volume_step"): vol.Any(
            "up",
            "down",
            vol.All(
                vol.Coerce(int),
                vol.Range(min=-100, max=100),
                lambda val: val / 100,
            ),
        ),
        # Optional name/area/floor slots handled by intent matcher
        vol.Optional("name"): cv.string,
        vol.Optional("area"): cv.string,
        vol.Optional("floor"): cv.string,
        vol.Optional("preferred_area_id"): cv.string,
        vol.Optional("preferred_floor_id"): cv.string,
    }
    platforms = {DOMAIN}

    async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
        """Handle the intent."""
        hass = intent_obj.hass
        component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN]

        slots = self.async_validate_slots(intent_obj.slots)
        volume_step = slots["volume_step"]["value"]

        # Entity name to match
        name_slot = slots.get("name", {})
        entity_name: str | None = name_slot.get("value")

        # Get area/floor info
        area_slot = slots.get("area", {})
        area_id = area_slot.get("value")

        floor_slot = slots.get("floor", {})
        floor_id = floor_slot.get("value")

        # Find matching entities
        match_constraints = intent.MatchTargetsConstraints(
            name=entity_name,
            area_name=area_id,
            floor_name=floor_id,
            domains={DOMAIN},
            assistant=intent_obj.assistant,
            features=MediaPlayerEntityFeature.VOLUME_SET,
        )
        match_preferences = intent.MatchTargetsPreferences(
            area_id=slots.get("preferred_area_id", {}).get("value"),
            floor_id=slots.get("preferred_floor_id", {}).get("value"),
        )
        match_result = intent.async_match_targets(
            hass, match_constraints, match_preferences
        )

        if not match_result.is_match:
            # No targets
            raise intent.MatchFailedError(
                result=match_result, constraints=match_constraints
            )

        if (
            match_result.is_match
            and (len(match_result.states) > 1)
            and ("name" not in intent_obj.slots)
        ):
            # Multiple targets not by name, so we need to check state
            match_result.states = [
                s for s in match_result.states if s.state == STATE_PLAYING
            ]
            if not match_result.states:
                # No media players are playing
                raise intent.MatchFailedError(
                    result=intent.MatchTargetsResult(
                        is_match=False, no_match_reason=intent.MatchFailedReason.STATE
                    ),
                    constraints=match_constraints,
                    preferences=match_preferences,
                )

        target_entity_ids = {s.entity_id for s in match_result.states}
        target_entities = [
            e for e in component.entities if e.entity_id in target_entity_ids
        ]

        if volume_step == "up":
            coros = [e.async_volume_up() for e in target_entities]
        elif volume_step == "down":
            coros = [e.async_volume_down() for e in target_entities]
        else:
            coros = [
                e.async_set_volume_level(
                    max(0.0, min(1.0, e.volume_level + volume_step))
                )
                for e in target_entities
            ]

        try:
            await asyncio.gather(*coros)
        except HomeAssistantError as err:
            _LOGGER.error("Error setting relative volume: %s", err)
            raise intent.IntentHandleError(
                f"Error setting relative volume: {err}"
            ) from err

        response = intent_obj.create_response()
        response.async_set_states(match_result.states)
        return response
