"""Platform for Control4 Rooms Media Players."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
import enum
import logging
from typing import Any

from pyControl4.error_handling import C4Exception
from pyControl4.room import C4Room

from homeassistant.components.media_player import (
    MediaPlayerDeviceClass,
    MediaPlayerEntity,
    MediaPlayerEntityFeature,
    MediaPlayerState,
    MediaType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from . import Control4Entity
from .const import CONF_DIRECTOR, CONF_DIRECTOR_ALL_ITEMS, CONF_UI_CONFIGURATION, DOMAIN
from .director_utils import update_variables_for_config_entry

_LOGGER = logging.getLogger(__name__)

CONTROL4_POWER_STATE = "POWER_STATE"
CONTROL4_VOLUME_STATE = "CURRENT_VOLUME"
CONTROL4_MUTED_STATE = "IS_MUTED"
CONTROL4_CURRENT_VIDEO_DEVICE = "CURRENT_VIDEO_DEVICE"
CONTROL4_PLAYING = "PLAYING"
CONTROL4_PAUSED = "PAUSED"
CONTROL4_STOPPED = "STOPPED"
CONTROL4_MEDIA_INFO = "CURRENT MEDIA INFO"

CONTROL4_PARENT_ID = "parentId"

VARIABLES_OF_INTEREST = {
    CONTROL4_POWER_STATE,
    CONTROL4_VOLUME_STATE,
    CONTROL4_MUTED_STATE,
    CONTROL4_CURRENT_VIDEO_DEVICE,
    CONTROL4_MEDIA_INFO,
    CONTROL4_PLAYING,
    CONTROL4_PAUSED,
    CONTROL4_STOPPED,
}


class _SourceType(enum.Enum):
    AUDIO = 1
    VIDEO = 2


@dataclass
class _RoomSource:
    """Class for Room Source."""

    source_type: set[_SourceType]
    idx: int
    name: str


async def get_rooms(hass: HomeAssistant, entry: ConfigEntry):
    """Return a list of all Control4 rooms."""
    director_all_items = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS]
    return [
        item
        for item in director_all_items
        if "typeName" in item and item["typeName"] == "room"
    ]


async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
    """Set up Control4 rooms from a config entry."""
    entry_data = hass.data[DOMAIN][entry.entry_id]
    ui_config = entry_data[CONF_UI_CONFIGURATION]

    # OS 2 will not have a ui_configuration
    if not ui_config:
        _LOGGER.debug("No UI Configuration found for Control4")
        return

    all_rooms = await get_rooms(hass, entry)
    if not all_rooms:
        return

    scan_interval = entry_data[CONF_SCAN_INTERVAL]
    _LOGGER.debug("Scan interval = %s", scan_interval)

    async def async_update_data() -> dict[int, dict[str, Any]]:
        """Fetch data from Control4 director."""
        try:
            return await update_variables_for_config_entry(
                hass, entry, VARIABLES_OF_INTEREST
            )
        except C4Exception as err:
            raise UpdateFailed(f"Error communicating with API: {err}") from err

    coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]](
        hass,
        _LOGGER,
        name="room",
        update_method=async_update_data,
        update_interval=timedelta(seconds=scan_interval),
    )

    # Fetch initial data so we have data when entities subscribe
    await coordinator.async_refresh()

    items_by_id = {
        item["id"]: item
        for item in hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS]
    }
    item_to_parent_map = {
        k: item["parentId"]
        for k, item in items_by_id.items()
        if "parentId" in item and k > 1
    }

    entity_list = []
    for room in all_rooms:
        room_id = room["id"]

        sources: dict[int, _RoomSource] = {}
        for exp in ui_config["experiences"]:
            if room_id == exp["room_id"]:
                exp_type = exp["type"]
                if exp_type not in ("listen", "watch"):
                    continue

                dev_type = (
                    _SourceType.AUDIO if exp_type == "listen" else _SourceType.VIDEO
                )
                for source in exp["sources"]["source"]:
                    dev_id = source["id"]
                    name = items_by_id.get(dev_id, {}).get(
                        "name", f"Unknown Device - {dev_id}"
                    )
                    if dev_id in sources:
                        sources[dev_id].source_type.add(dev_type)
                    else:
                        sources[dev_id] = _RoomSource(
                            source_type={dev_type}, idx=dev_id, name=name
                        )

        try:
            hidden = room["roomHidden"]
            entity_list.append(
                Control4Room(
                    entry_data,
                    coordinator,
                    room["name"],
                    room_id,
                    item_to_parent_map,
                    sources,
                    hidden,
                )
            )
        except KeyError:
            _LOGGER.exception(
                "Unknown device properties received from Control4: %s",
                room,
            )
            continue

    async_add_entities(entity_list, True)


class Control4Room(Control4Entity, MediaPlayerEntity):
    """Control4 Room entity."""

    _attr_has_entity_name = True

    def __init__(
        self,
        entry_data: dict,
        coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]],
        name: str,
        room_id: int,
        id_to_parent: dict[int, int],
        sources: dict[int, _RoomSource],
        room_hidden: bool,
    ) -> None:
        """Initialize Control4 room entity."""
        super().__init__(
            entry_data,
            coordinator,
            None,
            room_id,
            device_name=name,
            device_manufacturer=None,
            device_model=None,
            device_id=room_id,
        )
        self._attr_entity_registry_enabled_default = not room_hidden
        self._id_to_parent = id_to_parent
        self._sources = sources
        self._attr_supported_features = (
            MediaPlayerEntityFeature.PLAY
            | MediaPlayerEntityFeature.PAUSE
            | MediaPlayerEntityFeature.STOP
            | MediaPlayerEntityFeature.VOLUME_MUTE
            | MediaPlayerEntityFeature.VOLUME_SET
            | MediaPlayerEntityFeature.VOLUME_STEP
            | MediaPlayerEntityFeature.TURN_OFF
            | MediaPlayerEntityFeature.SELECT_SOURCE
        )

    def _create_api_object(self):
        """Create a pyControl4 device object.

        This exists so the director token used is always the latest one, without needing to re-init the entire entity.
        """
        return C4Room(self.entry_data[CONF_DIRECTOR], self._idx)

    def _get_device_from_variable(self, var: str) -> int | None:
        current_device = self.coordinator.data[self._idx][var]
        if current_device == 0:
            return None

        return current_device

    def _get_current_video_device_id(self) -> int | None:
        return self._get_device_from_variable(CONTROL4_CURRENT_VIDEO_DEVICE)

    def _get_current_playing_device_id(self) -> int | None:
        media_info = self._get_media_info()
        if media_info:
            if "medSrcDev" in media_info:
                return media_info["medSrcDev"]
            if "deviceid" in media_info:
                return media_info["deviceid"]
        return 0

    def _get_media_info(self) -> dict | None:
        """Get the Media Info Dictionary if populated."""
        media_info = self.coordinator.data[self._idx][CONTROL4_MEDIA_INFO]
        if "mediainfo" in media_info:
            return media_info["mediainfo"]
        return None

    def _get_current_source_state(self) -> str | None:
        current_source = self._get_current_playing_device_id()
        while current_source:
            current_data = self.coordinator.data.get(current_source, None)
            if current_data:
                if current_data.get(CONTROL4_PLAYING, None):
                    return MediaPlayerState.PLAYING
                if current_data.get(CONTROL4_PAUSED, None):
                    return MediaPlayerState.PAUSED
                if current_data.get(CONTROL4_STOPPED, None):
                    return MediaPlayerState.ON
            current_source = self._id_to_parent.get(current_source, None)
        return None

    @property
    def device_class(self) -> MediaPlayerDeviceClass | None:
        """Return the class of this entity."""
        for avail_source in self._sources.values():
            if _SourceType.VIDEO in avail_source.source_type:
                return MediaPlayerDeviceClass.TV
        return MediaPlayerDeviceClass.SPEAKER

    @property
    def state(self):
        """Return whether this room is on or idle."""

        if source_state := self._get_current_source_state():
            return source_state

        if self.coordinator.data[self._idx][CONTROL4_POWER_STATE]:
            return MediaPlayerState.ON

        return MediaPlayerState.IDLE

    @property
    def source(self):
        """Get the current source."""
        current_source = self._get_current_playing_device_id()
        if not current_source or current_source not in self._sources:
            return None
        return self._sources[current_source].name

    @property
    def media_title(self) -> str | None:
        """Get the Media Title."""
        media_info = self._get_media_info()
        if not media_info:
            return None
        if "title" in media_info:
            return media_info["title"]
        current_source = self._get_current_playing_device_id()
        if not current_source or current_source not in self._sources:
            return None
        return self._sources[current_source].name

    @property
    def media_content_type(self):
        """Get current content type if available."""
        current_source = self._get_current_playing_device_id()
        if not current_source:
            return None
        if current_source == self._get_current_video_device_id():
            return MediaType.VIDEO
        return MediaType.MUSIC

    async def async_media_play_pause(self):
        """If possible, toggle the current play/pause state.

        Not every source supports play/pause.
        Unfortunately MediaPlayer capabilities are not dynamic,
        so we must determine if play/pause is supported here
        """
        if self._get_current_source_state():
            await super().async_media_play_pause()

    @property
    def source_list(self) -> list[str]:
        """Get the available source."""
        return [x.name for x in self._sources.values()]

    @property
    def volume_level(self):
        """Get the volume level."""
        return self.coordinator.data[self._idx][CONTROL4_VOLUME_STATE] / 100

    @property
    def is_volume_muted(self):
        """Check if the volume is muted."""
        return bool(self.coordinator.data[self._idx][CONTROL4_MUTED_STATE])

    async def async_select_source(self, source):
        """Select a new source."""
        for avail_source in self._sources.values():
            if avail_source.name == source:
                audio_only = _SourceType.VIDEO not in avail_source.source_type
                if audio_only:
                    await self._create_api_object().setAudioSource(avail_source.idx)
                else:
                    await self._create_api_object().setVideoAndAudioSource(
                        avail_source.idx
                    )
                break

        await self.coordinator.async_request_refresh()

    async def async_turn_off(self):
        """Turn off the room."""
        await self._create_api_object().setRoomOff()
        await self.coordinator.async_request_refresh()

    async def async_mute_volume(self, mute):
        """Mute the room."""
        if mute:
            await self._create_api_object().setMuteOn()
        else:
            await self._create_api_object().setMuteOff()
        await self.coordinator.async_request_refresh()

    async def async_set_volume_level(self, volume):
        """Set room volume, 0-1 scale."""
        await self._create_api_object().setVolume(int(volume * 100))
        await self.coordinator.async_request_refresh()

    async def async_volume_up(self):
        """Increase the volume by 1."""
        await self._create_api_object().setIncrementVolume()
        await self.coordinator.async_request_refresh()

    async def async_volume_down(self):
        """Decrease the volume by 1."""
        await self._create_api_object().setDecrementVolume()
        await self.coordinator.async_request_refresh()

    async def async_media_pause(self):
        """Issue a pause command."""
        await self._create_api_object().setPause()
        await self.coordinator.async_request_refresh()

    async def async_media_play(self):
        """Issue a play command."""
        await self._create_api_object().setPlay()
        await self.coordinator.async_request_refresh()

    async def async_media_stop(self):
        """Issue a stop command."""
        await self._create_api_object().setStop()
        await self.coordinator.async_request_refresh()
