"""Actions for integration."""

from __future__ import annotations

from typing import TYPE_CHECKING

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import (
    HomeAssistant,
    ServiceCall,
    ServiceResponse,
    SupportsResponse,
    callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from music_assistant_models.errors import (
    InvalidCommand,
    MediaNotFoundError,
    ProviderUnavailableError,
)

from .const import (
    ATTR_COMMAND,
    ATTR_DATA,
    ATTR_DURATION,
    ATTR_FAVORITE,
    ATTR_LIMIT,
    ATTR_LIMIT_AFTER,
    ATTR_LIMIT_BEFORE,
    ATTR_LOCAL_IMAGE_ENCODED,
    ATTR_MEDIA_ALBUM_NAME,
    ATTR_MEDIA_ARTIST,
    ATTR_MEDIA_CONTENT_ID,
    ATTR_MEDIA_IMAGE,
    ATTR_MEDIA_TITLE,
    ATTR_OFFSET,
    ATTR_PLAYER_ENTITY,
    ATTR_POSITION,
    ATTR_PROVIDERS,
    ATTR_QUEUE_ID,
    ATTR_QUEUE_ITEM_ID,
    ATTR_RELEASE_DATE,
    ATTR_VOLUME_LEVEL,
    CONF_DOWNLOAD_LOCAL,
    DEFAULT_QUEUE_ITEMS_LIMIT,
    DEFAULT_QUEUE_ITEMS_OFFSET,
    DOMAIN,
    LOGGER,
    SERVICE_GET_GROUP_VOLUME,
    SERVICE_GET_QUEUE_ITEMS,
    SERVICE_GET_RECOMMENDATIONS,
    SERVICE_MOVE_QUEUE_ITEM_DOWN,
    SERVICE_MOVE_QUEUE_ITEM_NEXT,
    SERVICE_MOVE_QUEUE_ITEM_UP,
    SERVICE_PLAY_QUEUE_ITEM,
    SERVICE_REMOVE_QUEUE_ITEM,
    SERVICE_SEND_COMMAND,
    SERVICE_SET_GROUP_VOLUME,
)
from .controller import MassQueueController
from .schemas import (
    GET_GROUP_VOLUME_SERVICE_SCHEMA,
    GET_RECOMMENDATIONS_SERVICE_SCHEMA,
    MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA,
    MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
    MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA,
    PLAY_QUEUE_ITEM_SERVICE_SCHEMA,
    QUEUE_ITEM_SCHEMA,
    QUEUE_ITEMS_SERVICE_SCHEMA,
    REMOVE_QUEUE_ITEM_SERVICE_SCHEMA,
    SEND_COMMAND_SERVICE_SCHEMA,
    SET_GROUP_VOLUME_SERVICE_SCHEMA,
    TRACK_ITEM_SCHEMA,
)
from .utils import (
    find_image,
    parse_uri,
)

if TYPE_CHECKING:
    from music_assistant_client import MusicAssistantClient

    from . import MassQueueEntryData


class MassQueueActions:
    """Class to manage Music Assistant actions without passing `hass` and `mass_client` each time."""

    def __init__(
        self,
        hass: HomeAssistant,
        mass_client: MusicAssistantClient,
        config_entry: ConfigEntry,
    ):
        """Initialize class."""
        self._hass: HomeAssistant = hass
        self._client: MusicAssistantClient = mass_client
        self._controller = MassQueueController(self._hass, self._client, config_entry)
        self._config_entry = config_entry
        self._download_local = config_entry.options.get(CONF_DOWNLOAD_LOCAL)

    def setup_controller(self):
        """Setup Music Assistant controller."""
        self._controller.update_players()
        self._controller.subscribe_events()
        self._hass.loop.create_task(self._controller.update_queues())

    @callback
    def register_actions(self) -> None:
        """Register actions with Home Assistant."""
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_GET_QUEUE_ITEMS,
            self.get_queue_items,
            schema=QUEUE_ITEMS_SERVICE_SCHEMA,
            supports_response=SupportsResponse.ONLY,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_PLAY_QUEUE_ITEM,
            self.play_queue_item,
            schema=PLAY_QUEUE_ITEM_SERVICE_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )

        self._hass.services.async_register(
            DOMAIN,
            SERVICE_REMOVE_QUEUE_ITEM,
            self.remove_queue_item,
            schema=REMOVE_QUEUE_ITEM_SERVICE_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_MOVE_QUEUE_ITEM_UP,
            self.move_queue_item_up,
            schema=MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_MOVE_QUEUE_ITEM_DOWN,
            self.move_queue_item_down,
            schema=MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_MOVE_QUEUE_ITEM_NEXT,
            self.move_queue_item_next,
            schema=MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_SEND_COMMAND,
            self.send_command,
            schema=SEND_COMMAND_SERVICE_SCHEMA,
            supports_response=SupportsResponse.OPTIONAL,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_GET_RECOMMENDATIONS,
            self.get_recommendations,
            schema=GET_RECOMMENDATIONS_SERVICE_SCHEMA,
            supports_response=SupportsResponse.ONLY,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_GET_GROUP_VOLUME,
            self.get_group_volume,
            schema=GET_GROUP_VOLUME_SERVICE_SCHEMA,
            supports_response=SupportsResponse.ONLY,
        )
        self._hass.services.async_register(
            DOMAIN,
            SERVICE_SET_GROUP_VOLUME,
            self.set_group_volume,
            schema=SET_GROUP_VOLUME_SERVICE_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )

    def get_queue_id(self, entity_id: str):
        """Get the queue ID for a player."""
        return self._hass.states.get(entity_id).attributes[ATTR_QUEUE_ID]

    async def get_queue_index(self, entity_id: str):
        """Get the current index of the queue."""
        active_queue = await self.get_active_queue(entity_id)
        try:
            return active_queue.current_index or 0
        except AttributeError:
            return 0

    async def get_active_queue(self, entity_id: str):
        """Get active queue details."""
        queue_id = self.get_queue_id(entity_id)
        return await self._client.player_queues.get_active_queue(queue_id)

    async def _format_queue_item(self, queue_item: dict) -> dict:
        """Format list of queue items for response."""
        media = queue_item["media_item"]

        queue_item_id = queue_item["queue_item_id"]
        media_title = media["name"]
        media_album = media.get("album")
        media_album_name = "" if media_album is None else media_album.get("name", "")
        media_content_id = media["uri"]
        media_image = find_image(queue_item) or ""
        local_image_encoded = queue_item.get(ATTR_LOCAL_IMAGE_ENCODED)
        favorite = media["favorite"]

        artists = media["artists"]
        artist_names = [artist["name"] for artist in artists]
        media_artist = ", ".join(artist_names)
        response: ServiceResponse = QUEUE_ITEM_SCHEMA(
            {
                ATTR_QUEUE_ITEM_ID: queue_item_id,
                ATTR_MEDIA_TITLE: media_title,
                ATTR_MEDIA_ALBUM_NAME: media_album_name,
                ATTR_MEDIA_ARTIST: media_artist,
                ATTR_MEDIA_CONTENT_ID: media_content_id,
                ATTR_MEDIA_IMAGE: media_image,
                ATTR_FAVORITE: favorite,
            },
        )
        if local_image_encoded:
            response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded
        return response

    async def send_command(self, call: ServiceCall) -> ServiceResponse:
        """Sends command to Music Assistant and returns response."""
        command = call.data[ATTR_COMMAND]
        data = call.data.get(ATTR_DATA)
        response = await self._controller.send_command(command, data)
        return {"response": response}

    async def get_recommendations(self, call: ServiceCall) -> ServiceResponse:
        """Pulls all recommendations for the providers given."""
        providers = call.data.get(ATTR_PROVIDERS)
        return await self._controller.get_recommendations(providers)

    async def get_group_volume(self, call: ServiceCall) -> ServiceResponse:
        """Gets the group volume for a single player."""
        entity_id = call.data.get(ATTR_PLAYER_ENTITY)
        queue_id = self.get_queue_id(entity_id)
        try:
            volume = await self._controller.get_grouped_volume(queue_id)
        except:  # noqa: E722
            volume = None
        return volume

    async def set_group_volume(self, call: ServiceCall) -> ServiceResponse:
        """Sets the group volume for a player."""
        entity_id = call.data.get(ATTR_PLAYER_ENTITY)
        queue_id = self.get_queue_id(entity_id)
        volume_level = call.data.get(ATTR_VOLUME_LEVEL)
        await self._controller.set_grouped_volume(queue_id, volume_level)

    async def get_queue_items(self, call: ServiceCall) -> ServiceResponse:
        """Get all items in queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        queue_id = self.get_queue_id(entity_id)
        if queue_id is None:
            return {entity_id: []}
        offset = call.data.get(ATTR_OFFSET)
        limit = call.data.get(ATTR_LIMIT)
        limit_before = call.data.get(ATTR_LIMIT_BEFORE)
        limit_after = call.data.get(ATTR_LIMIT_AFTER)
        idx = await self.get_queue_index(entity_id)
        if limit_before:
            offset = idx - limit_before
        if limit_after:
            limit = limit_before + limit_after + 1 if limit_before else limit_after + 1
        if offset is None:
            offset = idx + DEFAULT_QUEUE_ITEMS_OFFSET
        if limit is None:
            limit = DEFAULT_QUEUE_ITEMS_LIMIT
        offset = max(offset, 0)
        queue_items = await self._controller.player_queue(queue_id, limit, offset)
        response: ServiceResponse = {
            entity_id: [await self._format_queue_item(item) for item in queue_items],
        }
        return response

    async def play_queue_item(self, call: ServiceCall) -> ServiceResponse:
        """Play selected item in queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
        queue_id = self.get_queue_id(entity_id)
        await self._client.send_command(
            "player_queues/play_index",
            queue_id=queue_id,
            index=queue_item_id,
        )

    async def remove_queue_item(self, call: ServiceCall) -> ServiceResponse:
        """Remove selected item from queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
        queue_id = self.get_queue_id(entity_id)
        await self._client.player_queues.queue_command_delete(queue_id, queue_item_id)

    async def move_queue_item_up(self, call: ServiceCall) -> ServiceResponse:
        """Move selected item up in queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
        queue_id = self.get_queue_id(entity_id)
        await self._client.player_queues.queue_command_move_up(queue_id, queue_item_id)

    async def move_queue_item_down(self, call: ServiceCall) -> ServiceResponse:
        """Move selected item down in queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
        queue_id = self.get_queue_id(entity_id)
        await self._client.player_queues.queue_command_move_down(
            queue_id,
            queue_item_id,
        )

    async def move_queue_item_next(self, call: ServiceCall) -> ServiceResponse:
        """Move selected item next in queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
        queue_id = self.get_queue_id(entity_id)
        await self._client.player_queues.queue_command_move_next(
            queue_id,
            queue_item_id,
        )

    async def unfavorite_item(self, call: ServiceCall) -> ServiceResponse:
        """Unfavorites currently playing item in queue."""
        entity_id = call.data[ATTR_PLAYER_ENTITY]
        attrs = self._hass.states.get(entity_id).attributes
        content_id = attrs.get(ATTR_MEDIA_CONTENT_ID)
        if not content_id:
            msg = f"Cannot find media with content id {content_id}"
            raise MediaNotFoundError(msg)
        provider = content_id.split("://")[0]
        if provider != "library":
            msg = f"Unfavorite can only apply to library media items, not from provider {provider}"
            raise InvalidCommand(msg)
        item_id = str(content_id.split("/")[-1])
        await self._client.send_command(
            "music/favorites/remove_item",
            media_type="track",
            library_item_id=item_id,
        )

    async def get_artist_details(self, artist_uri):
        """Retrieves the details for an artist."""
        provider, item_id = parse_uri(artist_uri)
        LOGGER.debug(f"Getting artist details for provider {provider}")
        return await self._client.music.get_artist(item_id, provider)

    async def get_album_details(self, album_uri):
        """Retrieves the details for an album."""
        provider, item_id = parse_uri(album_uri)
        LOGGER.debug(f"Getting album details for provider {provider}")
        return await self._client.music.get_album(item_id, provider)

    async def get_playlist_details(self, playlist_uri):
        """Retrieves the details for a playlist."""
        provider, item_id = parse_uri(playlist_uri)
        LOGGER.debug(f"Getting album details for provider {provider}")
        return await self._client.music.get_playlist(item_id, provider)

    async def get_podcast_details(self, podcast_uri):
        """Retrieves the details for a podcast."""
        provider, item_id = parse_uri(podcast_uri)
        LOGGER.debug(f"Getting podcast details for provider {provider}")
        return await self._client.music.get_podcast(item_id, provider)

    async def get_artist_tracks(self, artist_uri: str, page: int | None = None):
        """Retrieves a limited number of tracks from an artist."""
        details = await self.get_artist_details(artist_uri)
        mappings = list(details.provider_mappings)
        if not len(mappings) > 0:
            msg = f"URI {artist_uri} returned no results!"
            raise ProviderUnavailableError(msg)
        mapping = mappings[0]
        item_id = mapping.item_id
        provider = mapping.provider_domain
        resp = (
            await self._client.music.get_artist_tracks(item_id, provider)
            if not page
            else await self._client.music.get_artist_tracks(item_id, provider, page)
        )
        return [self.format_track_item(item.to_dict()) for item in resp]

    async def get_album_tracks(self, album_uri: str, page: int | None = None):
        """Retrieves all tracks from an album."""
        details = await self.get_album_details(album_uri)
        mappings = list(details.provider_mappings)
        if not len(mappings) > 0:
            msg = f"URI {album_uri} returned no results!"
            raise ProviderUnavailableError(msg)
        mapping = mappings[0]
        item_id = mapping.item_id
        provider = mapping.provider_domain
        resp = (
            await self._client.music.get_album_tracks(item_id, provider)
            if not page
            else await self._client.music.get_album_tracks(item_id, provider, page)
        )
        return [self.format_track_item(item.to_dict()) for item in resp]

    async def get_podcast_episodes(self, podcast_uri):
        """Retrieves all episodes for a podcast."""
        provider, item_id = parse_uri(podcast_uri)
        LOGGER.debug(
            f"Getting podcast episodes for provider {provider}, item_id {item_id}",
        )
        resp: list = await self._client.music.get_podcast_episodes(item_id, provider)
        formatted = [self.format_podcast_episode(item.to_dict()) for item in resp]
        formatted.sort(key=lambda x: x[ATTR_RELEASE_DATE], reverse=True)
        return formatted

    async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None):
        """Retrieves all playlist items."""
        provider, item_id = parse_uri(playlist_uri)
        LOGGER.debug(
            f"Getting playlist items for provider {provider}, item_id {item_id}",
        )
        resp = (
            await self._client.music.get_playlist_tracks(item_id, provider)
            if not page
            else await self._client.music.get_playlist_tracks(item_id, provider, page)
        )
        return [self.format_playlist_track(item.to_dict()) for item in resp]

    def format_playlist_track(self, playlist_track: dict) -> TRACK_ITEM_SCHEMA:
        """Processes individual playlist tracks using format_track_item and adds position."""
        result = self.format_track_item(playlist_track)
        result[ATTR_POSITION] = playlist_track["position"]
        return result

    def format_track_item(self, track_item: dict) -> TRACK_ITEM_SCHEMA:
        """Process an individual track item."""
        result = self.format_item(track_item)
        media_album = track_item.get("album")
        media_album_name = "" if media_album is None else media_album.get("name", "")
        artists = track_item["artists"]
        artist_names = [artist["name"] for artist in artists]
        media_artist = ", ".join(artist_names)
        result[ATTR_MEDIA_ALBUM_NAME] = media_album_name
        result[ATTR_MEDIA_ARTIST] = media_artist
        return result

    def format_podcast_episode(self, podcast_episode: dict) -> TRACK_ITEM_SCHEMA:
        """Process an individual track item."""
        result = self.format_item(podcast_episode)
        result[ATTR_RELEASE_DATE] = podcast_episode.get("metadata", {}).get(
            "release_date",
        )
        return result

    def format_item(self, media_item: dict) -> TRACK_ITEM_SCHEMA:
        """Processes the individual items in a playlist."""
        media_title = media_item.get("name") or "N/A"
        media_content_id = media_item["uri"]
        media_image = find_image(media_item) or ""
        local_image_encoded = media_item.get(ATTR_LOCAL_IMAGE_ENCODED)
        favorite = media_item["favorite"]
        duration = media_item["duration"] or 0
        response: ServiceResponse = TRACK_ITEM_SCHEMA(
            {
                ATTR_MEDIA_TITLE: media_title,
                ATTR_MEDIA_CONTENT_ID: media_content_id,
                ATTR_DURATION: duration,
                ATTR_MEDIA_IMAGE: media_image,
                ATTR_FAVORITE: favorite,
            },
        )
        if local_image_encoded:
            response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded
        return response

    async def remove_playlist_tracks(
        self,
        playlist_id: str | int,
        positions_to_remove: list[int],
    ):
        """Removes one or more items from a playlist."""
        await self._client.music.remove_playlist_tracks(
            playlist_id,
            positions_to_remove,
        )


@callback
def get_music_assistant_client(
    hass: HomeAssistant,
    entity_id: str,
) -> MusicAssistantClient:
    """Get Music Assistant client from entity_id."""
    registry = er.async_get(hass)
    entity = registry.async_get(entity_id)
    config_entry_id = entity.config_entry_id
    return _get_music_assistant_client(hass, config_entry_id)


@callback
def _get_music_assistant_client(
    hass: HomeAssistant,
    config_entry_id: str,
) -> MusicAssistantClient:
    """Get Music Assistant Client from config_entry_id."""
    entry: MassQueueEntryData | None
    if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
        exc = "Entry not found."
        raise ServiceValidationError(exc)
    if entry.state is not ConfigEntryState.LOADED:
        exc = "Entry not loaded"
        raise ServiceValidationError(exc)
    return entry.runtime_data.mass


@callback
async def setup_controller_and_actions(
    hass: HomeAssistant,
    mass_client: MusicAssistantClient,
    entry: ConfigEntry,
) -> MassQueueActions:
    """Initialize client and actions class, add actions to Home Assistant."""
    actions = MassQueueActions(hass, mass_client, entry)
    actions.setup_controller()
    return actions
