"""Support for LinkPlay media players."""

from __future__ import annotations

from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any

from linkplay.bridge import LinkPlayBridge
from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus
from linkplay.controller import LinkPlayController, LinkPlayMultiroom
from linkplay.exceptions import LinkPlayRequestException

from homeassistant.components import media_source
from homeassistant.components.media_player import (
    BrowseMedia,
    MediaPlayerDeviceClass,
    MediaPlayerEntity,
    MediaPlayerEntityFeature,
    MediaPlayerState,
    MediaType,
    RepeatMode,
    async_process_play_media_url,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import utcnow

from . import SHARED_DATA, LinkPlayConfigEntry
from .const import DOMAIN
from .entity import LinkPlayBaseEntity, exception_wrap

_LOGGER = logging.getLogger(__name__)
STATE_MAP: dict[PlayingStatus, MediaPlayerState] = {
    PlayingStatus.STOPPED: MediaPlayerState.IDLE,
    PlayingStatus.PAUSED: MediaPlayerState.PAUSED,
    PlayingStatus.PLAYING: MediaPlayerState.PLAYING,
    PlayingStatus.LOADING: MediaPlayerState.BUFFERING,
}

SOURCE_MAP: dict[PlayingMode, str] = {
    PlayingMode.NETWORK: "Wifi",
    PlayingMode.LINE_IN: "Line In",
    PlayingMode.BLUETOOTH: "Bluetooth",
    PlayingMode.OPTICAL: "Optical",
    PlayingMode.LINE_IN_2: "Line In 2",
    PlayingMode.USB_DAC: "USB DAC",
    PlayingMode.COAXIAL: "Coaxial",
    PlayingMode.XLR: "XLR",
    PlayingMode.HDMI: "HDMI",
    PlayingMode.OPTICAL_2: "Optical 2",
    PlayingMode.EXTERN_BLUETOOTH: "External Bluetooth",
    PlayingMode.PHONO: "Phono",
    PlayingMode.ARC: "ARC",
    PlayingMode.COAXIAL_2: "Coaxial 2",
    PlayingMode.TF_CARD_1: "SD Card 1",
    PlayingMode.TF_CARD_2: "SD Card 2",
    PlayingMode.CD: "CD",
    PlayingMode.DAB: "DAB Radio",
    PlayingMode.FM: "FM Radio",
    PlayingMode.RCA: "RCA",
    PlayingMode.UDISK: "USB",
    PlayingMode.SPOTIFY: "Spotify",
    PlayingMode.TIDAL: "Tidal",
    PlayingMode.FOLLOWER: "Follower",
}

SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}

REPEAT_MAP: dict[LoopMode, RepeatMode] = {
    LoopMode.CONTINOUS_PLAY_ONE_SONG: RepeatMode.ONE,
    LoopMode.PLAY_IN_ORDER: RepeatMode.OFF,
    LoopMode.CONTINUOUS_PLAYBACK: RepeatMode.ALL,
    LoopMode.RANDOM_PLAYBACK: RepeatMode.ALL,
    LoopMode.LIST_CYCLE: RepeatMode.ALL,
    LoopMode.SHUFF_DISABLED_REPEAT_DISABLED: RepeatMode.OFF,
    LoopMode.SHUFF_ENABLED_REPEAT_ENABLED_LOOP_ONCE: RepeatMode.ALL,
}

REPEAT_MAP_INV: dict[RepeatMode, LoopMode] = {v: k for k, v in REPEAT_MAP.items()}

EQUALIZER_MAP_INV: dict[str, EqualizerMode] = {
    mode.value: mode for mode in EqualizerMode
}

DEFAULT_FEATURES: MediaPlayerEntityFeature = (
    MediaPlayerEntityFeature.PLAY
    | MediaPlayerEntityFeature.PLAY_MEDIA
    | MediaPlayerEntityFeature.BROWSE_MEDIA
    | MediaPlayerEntityFeature.PAUSE
    | MediaPlayerEntityFeature.STOP
    | MediaPlayerEntityFeature.VOLUME_MUTE
    | MediaPlayerEntityFeature.VOLUME_SET
    | MediaPlayerEntityFeature.SELECT_SOURCE
    | MediaPlayerEntityFeature.SELECT_SOUND_MODE
    | MediaPlayerEntityFeature.GROUPING
)

SEEKABLE_FEATURES: MediaPlayerEntityFeature = (
    MediaPlayerEntityFeature.PREVIOUS_TRACK
    | MediaPlayerEntityFeature.NEXT_TRACK
    | MediaPlayerEntityFeature.REPEAT_SET
    | MediaPlayerEntityFeature.SEEK
)

RETRY_POLL_MAXIMUM = 3
SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 1


async def async_setup_entry(
    hass: HomeAssistant,
    entry: LinkPlayConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up a media player from a config entry."""
    async_add_entities([LinkPlayMediaPlayerEntity(entry.runtime_data.bridge)])


class LinkPlayMediaPlayerEntity(LinkPlayBaseEntity, MediaPlayerEntity):
    """Representation of a LinkPlay media player."""

    _attr_device_class = MediaPlayerDeviceClass.RECEIVER
    _attr_media_content_type = MediaType.MUSIC
    _attr_name = None

    def __init__(self, bridge: LinkPlayBridge) -> None:
        """Initialize the LinkPlay media player."""

        super().__init__(bridge)
        self._attr_unique_id = bridge.device.uuid
        self._retry_count = 0

        self._attr_source_list = [
            SOURCE_MAP[playing_mode] for playing_mode in bridge.device.playmode_support
        ]
        self._attr_sound_mode_list = [
            mode.value for mode in bridge.player.available_equalizer_modes
        ]

    async def async_added_to_hass(self) -> None:
        """Handle common setup when added to hass."""
        await super().async_added_to_hass()
        self.hass.data[DOMAIN][SHARED_DATA].entity_to_bridge[self.entity_id] = (
            self._bridge.device.uuid
        )

    @exception_wrap
    async def async_update(self) -> None:
        """Update the state of the media player."""
        try:
            await self._bridge.player.update_status()
            self._retry_count = 0
            self._update_properties()
        except LinkPlayRequestException:
            self._retry_count += 1
            if self._retry_count >= RETRY_POLL_MAXIMUM:
                self._attr_available = False

    @exception_wrap
    async def async_select_source(self, source: str) -> None:
        """Select input source."""
        await self._bridge.player.set_play_mode(SOURCE_MAP_INV[source])

    @exception_wrap
    async def async_select_sound_mode(self, sound_mode: str) -> None:
        """Select sound mode."""
        await self._bridge.player.set_equalizer_mode(EQUALIZER_MAP_INV[sound_mode])

    @exception_wrap
    async def async_mute_volume(self, mute: bool) -> None:
        """Mute the volume."""
        if mute:
            await self._bridge.player.mute()
        else:
            await self._bridge.player.unmute()

    @exception_wrap
    async def async_set_volume_level(self, volume: float) -> None:
        """Set volume level, range 0..1."""
        await self._bridge.player.set_volume(int(volume * 100))

    @exception_wrap
    async def async_media_pause(self) -> None:
        """Send pause command."""
        await self._bridge.player.pause()

    @exception_wrap
    async def async_media_play(self) -> None:
        """Send play command."""
        await self._bridge.player.resume()

    @exception_wrap
    async def async_media_stop(self) -> None:
        """Send stop command."""
        await self._bridge.player.stop()

    @exception_wrap
    async def async_media_next_track(self) -> None:
        """Send next command."""
        await self._bridge.player.next()

    @exception_wrap
    async def async_media_previous_track(self) -> None:
        """Send previous command."""
        await self._bridge.player.previous()

    @exception_wrap
    async def async_set_repeat(self, repeat: RepeatMode) -> None:
        """Set repeat mode."""
        await self._bridge.player.set_loop_mode(REPEAT_MAP_INV[repeat])

    async def async_browse_media(
        self,
        media_content_type: MediaType | str | None = None,
        media_content_id: str | None = None,
    ) -> BrowseMedia:
        """Return a BrowseMedia instance.

        The BrowseMedia instance will be used by the
        "media_player/browse_media" websocket command.
        """
        return await media_source.async_browse_media(
            self.hass,
            media_content_id,
            # This allows filtering content. In this case it will only show audio sources.
            content_filter=lambda item: item.media_content_type.startswith("audio/"),
        )

    @exception_wrap
    async def async_play_media(
        self, media_type: MediaType | str, media_id: str, **kwargs: Any
    ) -> None:
        """Play a piece of media."""
        if media_source.is_media_source_id(media_id):
            play_item = await media_source.async_resolve_media(
                self.hass, media_id, self.entity_id
            )
            media_id = play_item.url

        url = async_process_play_media_url(self.hass, media_id)
        await self._bridge.player.play(url)

    @exception_wrap
    async def async_play_preset(self, preset_number: int) -> None:
        """Play preset number."""
        try:
            await self._bridge.player.play_preset(preset_number)
        except ValueError as err:
            raise HomeAssistantError(err) from err

    @exception_wrap
    async def async_media_seek(self, position: float) -> None:
        """Seek to a position."""
        await self._bridge.player.seek(round(position))

    @exception_wrap
    async def async_join_players(self, group_members: list[str]) -> None:
        """Join `group_members` as a player group with the current player."""

        controller: LinkPlayController = self.hass.data[DOMAIN][SHARED_DATA].controller
        multiroom = self._bridge.multiroom
        if multiroom is None:
            multiroom = LinkPlayMultiroom(self._bridge)

        for group_member in group_members:
            bridge = await self._get_linkplay_bridge(group_member)
            if bridge:
                await multiroom.add_follower(bridge)

        await controller.discover_multirooms()

    async def _get_linkplay_bridge(self, entity_id: str) -> LinkPlayBridge:
        """Get linkplay bridge from entity_id."""

        shared_data = self.hass.data[DOMAIN][SHARED_DATA]
        controller = shared_data.controller
        bridge_uuid = shared_data.entity_to_bridge.get(entity_id, None)
        bridge = await controller.find_bridge(bridge_uuid)

        if bridge is None:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="invalid_grouping_entity",
                translation_placeholders={"entity_id": entity_id},
            )

        return bridge

    @property
    def group_members(self) -> list[str]:
        """List of players which are grouped together."""
        multiroom = self._bridge.multiroom
        if multiroom is None:
            return []

        shared_data = self.hass.data[DOMAIN][SHARED_DATA]
        leader_id: str | None = None
        followers = []

        # find leader and followers
        for ent_id, uuid in shared_data.entity_to_bridge.items():
            if uuid == multiroom.leader.device.uuid:
                leader_id = ent_id
            elif uuid in {f.device.uuid for f in multiroom.followers}:
                followers.append(ent_id)

        if TYPE_CHECKING:
            assert leader_id is not None
        return [leader_id, *followers]

    @property
    def media_image_url(self) -> str | None:
        """Image url of playing media."""
        if self._bridge.player.status in [PlayingStatus.PLAYING, PlayingStatus.PAUSED]:
            return str(self._bridge.player.album_art)
        return None

    @exception_wrap
    async def async_unjoin_player(self) -> None:
        """Remove this player from any group."""
        controller: LinkPlayController = self.hass.data[DOMAIN][SHARED_DATA].controller

        multiroom = self._bridge.multiroom
        if multiroom is not None:
            await multiroom.remove_follower(self._bridge)

        await controller.discover_multirooms()

    def _update_properties(self) -> None:
        """Update the properties of the media player."""
        self._attr_available = True
        self._attr_state = STATE_MAP[self._bridge.player.status]
        self._attr_volume_level = self._bridge.player.volume / 100
        self._attr_is_volume_muted = self._bridge.player.muted
        self._attr_repeat = REPEAT_MAP[self._bridge.player.loop_mode]
        self._attr_shuffle = self._bridge.player.loop_mode == LoopMode.RANDOM_PLAYBACK
        self._attr_sound_mode = self._bridge.player.equalizer_mode.value
        self._attr_supported_features = DEFAULT_FEATURES

        if self._bridge.player.status == PlayingStatus.PLAYING:
            if self._bridge.player.total_length != 0:
                self._attr_supported_features = (
                    self._attr_supported_features | SEEKABLE_FEATURES
                )

            self._attr_source = SOURCE_MAP.get(self._bridge.player.play_mode, "other")
            self._attr_media_position = self._bridge.player.current_position_in_seconds
            self._attr_media_position_updated_at = utcnow()
            self._attr_media_duration = self._bridge.player.total_length_in_seconds
            self._attr_media_artist = self._bridge.player.artist
            self._attr_media_title = self._bridge.player.title
            self._attr_media_album_name = self._bridge.player.album
        elif self._bridge.player.status == PlayingStatus.STOPPED:
            self._attr_media_position = None
            self._attr_media_position_updated_at = None
            self._attr_media_artist = None
            self._attr_media_title = None
            self._attr_media_album_name = None
