# ty:ignore[unresolved-import]
"""Controller for queues, players cache."""

from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING

from homeassistant.core import callback

if TYPE_CHECKING:
    from homeassistant.config_entries import ConfigEntry
    from homeassistant.core import HomeAssistant
    from music_assistant_client import MusicAssistantClient

from music_assistant_models.enums import EventType

from .const import (
    CONF_DOWNLOAD_LOCAL,
    DEFAULT_QUEUE_ITEMS_LIMIT,
    DEFAULT_QUEUE_ITEMS_OFFSET,
    LOGGER,
    MASS_QUEUE_EVENT_DOMAIN,
    MUSIC_ASSISTANT_EVENT_DOMAIN,
)
from .utils import (
    download_and_encode_image,
    find_image,
    format_queue_updated_event_data,
    generate_image_url_from_image_data,
    get_queue_id_from_player_data,
)


class MassQueueController:
    """Controller to hold methods, handle events, and control caches of players and queues."""

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

    # Events
    def subscribe_events(self):
        """Subscribe to Music Assistant events."""
        self._client.subscribe(self.on_queue_update_event, EventType.QUEUE_UPDATED)
        self._client.subscribe(
            self.on_queue_items_update_event,
            EventType.QUEUE_ITEMS_UPDATED,
        )
        self._client.subscribe(self.on_player_event, EventType.PLAYER_UPDATED)

    def send_ha_event(self, event_data):
        """Send event to Home Assistant."""
        LOGGER.debug(
            f"Sending event type {MUSIC_ASSISTANT_EVENT_DOMAIN}, data {event_data}",
        )
        self._hass.bus.async_fire(MUSIC_ASSISTANT_EVENT_DOMAIN, event_data)

    def on_queue_update_event(self, event):
        """Callback when queue update event is received."""
        LOGGER.debug("Got updated queue.")
        event_type = event.event
        event_object_id = event.object_id
        event_data = event.data
        event_queue_id = event_data.get("queue_id")
        self._hass.loop.create_task(self.update_queue_items(event_queue_id))
        if event_data is None:
            LOGGER.error(f"Event data is empty! Event: {event}")
            return
        data = format_queue_updated_event_data(event_data)
        ha_event_data = {"type": event_type, "object_id": event_object_id, "data": data}
        self.send_ha_event(ha_event_data)

    def on_queue_items_update_event(self, event):
        """Callback when queue items update event is received."""
        LOGGER.debug("Got updated queue items.")
        event_type = event.event
        event_object_id = event.object_id
        event_data = event.data
        event_queue_id = event_data.get("queue_id")
        self._hass.loop.create_task(self.update_queue_items(event_queue_id))
        if event_data is None:
            LOGGER.error(f"Event data is empty! Event: {event}")
            return
        data = format_queue_updated_event_data(event_data)
        ha_event_data = {"type": event_type, "object_id": event_object_id, "data": data}
        self.send_ha_event(ha_event_data)

    def on_player_event(self, event):
        """Callback when player event is received."""
        event_type = event.event
        event_object_id = event.object_id
        event_data = event.data
        event_player = event_data["player_id"]
        self.update_player_queue(event_player)
        if event_data is None:
            LOGGER.error(f"Event data is empty! Event: {event}")
            return
        ha_event_data = {
            "type": event_type,
            "object_id": event_object_id,
            "data": event.data,
        }
        self.send_ha_event(ha_event_data)

    # All players
    def get_all_players(self):
        """Get all Music Assistant players."""
        players = self._client.players.players
        result = {}
        for player_data in players:
            player_id = player_data.player_id
            queue_id = get_queue_id_from_player_data(player_data)
            result[player_id] = queue_id
        return result

    def update_players(self):
        """Update all Music Assistant players."""
        LOGGER.debug("Updating all players.")
        players = self.get_all_players()
        self.players.batch_add(players)

    # Individual players
    def update_player_queue(self, player_id: str):
        """Update queue items for single Music Assistant queue."""
        LOGGER.debug(f"Updating player {player_id}.")
        player = self._client.players.get(player_id)
        if player is None:
            self.players.remove(player_id)
        queue_id = get_queue_id_from_player_data(player)
        self.players.update(player_id, queue_id)

    async def send_command(self, command: str, data: dict | None = None):
        """Sends command to Music Assistant and returns response."""
        data = data or {}
        return await self._client.send_command(command, require_schema=None, **data)

    async def get_recommendations(self, providers: list | None = None):
        """Pulls all recommendations."""
        recs = await self._client.music.recommendations()
        if not providers:
            return recs
        rec_providers = []
        for rec in recs:
            if rec.provider not in rec_providers:
                rec_providers.append(rec.provider)

        used_rec_providers = [
            rec_provider
            for rec_provider in rec_providers
            for provider in providers
            if rec_provider.startswith(provider)
        ]
        return [rec for rec in recs if rec.provider in used_rec_providers]

    async def get_grouped_volume(self, player_id: str):
        """Get the grouped volume for a given player."""
        return self._client.players.get(player_id).group_volume

    async def set_grouped_volume(self, player_id: str, volume_level: int):
        """Sets the grouped volume for a given player."""
        await self._client.players.set_player_group_volume(player_id, volume_level)

    async def get_player_queue(self, player_id: str):
        """Gets queue items for single Music Assistant queue."""
        player = self._client.players.get(player_id)
        queue_id = get_queue_id_from_player_data(player)
        return await self.get_queue(queue_id)

    # All queues
    async def get_all_queues(self):
        """Gets queue items for all Music Assistant queues."""
        queue_ids = [q.queue_id for q in self._client.player_queues.player_queues]
        return {queue_id: await self.get_queue(queue_id) for queue_id in queue_ids}

    async def update_queues(self):
        """Update queue items for all Music Assistant queues."""
        LOGGER.debug("Updating all queues.")
        queues = await self.get_all_queues()
        self.queues.batch_add(queues)

    # Individual queues
    async def player_queue(
        self,
        queue_id: str,
        limit: int = DEFAULT_QUEUE_ITEMS_LIMIT,
        offset: int = DEFAULT_QUEUE_ITEMS_OFFSET,
    ):
        """Get the cached queue items for a single queue."""
        queue = self.queues.get(queue_id)
        if offset == -1:
            try:
                offset = await self.get_queue_index(queue_id) - 5
            except IndexError:
                offset = 0
        offset = max(offset, 0)
        return queue[offset : offset + limit] if queue else []

    async def update_queue_items(self, queue_id: str):
        """Update the queue items for a single queue."""
        LOGGER.debug(f"Updating queue {queue_id}.")
        queue = await self.get_queue(queue_id)
        self.queues.update(queue_id, queue)

    async def get_queue(
        self,
        queue_id: str,
        limit: int = DEFAULT_QUEUE_ITEMS_LIMIT,
        offset: int = DEFAULT_QUEUE_ITEMS_OFFSET,
    ):
        """Get the queue items for a single queue."""
        if offset == -1:
            try:
                offset = await self.get_queue_index(queue_id) - 5
            except IndexError:
                offset = 0
        offset = max(offset, 0)
        # HA 2025.12 Fix: `get_player_queue_items` replaced with `get_queue_items`
        try:
            return await self._client.player_queues.get_queue_items(
                queue_id=queue_id,
                limit=limit,
                offset=offset,
            )
        except AttributeError:
            return await self._client.player_queues.get_player_queue_items(
                queue_id=queue_id,
                limit=limit,
                offset=offset,
            )

    async def get_active_queue(self, queue_id: str):
        """Get the active queue for a single queue."""
        return await self._client.player_queues.get_active_queue(queue_id)

    async def get_queue_index(self, queue_id: str):
        """Get the active queue index for a single queue."""
        active_queue = await self.get_active_queue(queue_id)
        return active_queue.current_index


class Players:
    """Class to hold all player caches."""

    def __init__(self, hass: HomeAssistant, players: dict | None = None):
        """Initialize class."""
        self.players = players if players is not None else {}
        self._hass = hass

    def get(self, player_id):
        """Returns cached player records."""
        return self.players.get(player_id)

    def add(self, player_id: str, queue_id: str | None):
        """Adds a single player."""
        self.players[player_id] = queue_id
        event_data = {
            "type": "player_added",
            "data": {"player_id": player_id, "queue_id": queue_id},
        }
        self.send_ha_event(event_data)

    def batch_add(self, players: dict):
        """Adds multiple players at once."""
        for k, v in players.items():
            self.players[k] = v
        event_data = {"type": "player_added", "data": {"players": players}}
        self.send_ha_event(event_data)

    def remove(self, player_id: str):
        """Removes a single player."""
        if player_id in self.players:
            self.players.pop(player_id)
        event_data = {
            "type": "player_removed",
            "data": {
                "player_id": player_id,
            },
        }
        self.send_ha_event(event_data)

    def update(self, player_id: str, queue_id: str):
        """Updates the queue ID of a single player."""
        if player_id not in self.players:
            return
        current_queue_id = self.players[player_id]
        if current_queue_id == queue_id:
            pass
        self.players[player_id] = queue_id
        event_data = {
            "type": "player_updated",
            "data": {"player_id": player_id, "queue_id": queue_id},
        }
        self.send_ha_event(event_data)

    def send_ha_event(self, event_data):
        """Send event to Home Assistant."""
        LOGGER.debug(
            f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}",
        )
        self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data)


class Queues:
    """Class to hold all queue caches."""

    def __init__(
        self,
        hass: HomeAssistant,
        client: MusicAssistantClient,
        config_entry: ConfigEntry,
        queues: dict | None = None,
    ):
        """Initialize class."""
        self.queues = self.batch_add(queues) if queues else {}
        self._hass = hass
        self._config_entry = config_entry
        self._download_local = config_entry.options.get(CONF_DOWNLOAD_LOCAL)
        self._client = client

    def get(self, queue_id):
        """Returns cached queue records."""
        return self.queues.get(queue_id, [])

    def add(self, queue_id: str, queue_items: int):
        """Adds a single queue."""
        self.process_queue_images(queue_items, queue_id)
        event_data = {"type": "queue_added", "data": {"queue_id": queue_id}}
        self.send_ha_event(event_data)

    def batch_add(self, queues):
        """Adds multiple queues at once."""
        for k, v in queues.items():
            self.process_queue_images(v, k)
        event_data = {"type": "queues_added", "data": {"queue_id": list(queues.keys())}}
        self.send_ha_event(event_data)

    def update(self, queue_id, queue_items):
        """Updates queue items in record."""
        self.queues[queue_id] = self.process_queue_images(queue_items, queue_id)
        event_data = {"type": "queue_updated", "data": {"queue_id": queue_id}}
        self.send_ha_event(event_data)

    def remove(self, queue_id):
        """Removes queue from record."""
        if queue_id not in self.queues:
            return
        self.queues.pop(queue_id)
        event_data = {"type": "queue_removed", "data": {"queue_id": queue_id}}
        self.send_ha_event(event_data)

    def send_ha_event(self, event_data):
        """Send event to Home Assistant."""
        LOGGER.debug(
            f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}",
        )
        self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data)

    async def process_image_single_item(self, queue_item: dict):
        """Processes the images from a single item."""
        media_image = find_image(queue_item)
        if media_image:
            queue_item["media_image"] = media_image
        else:
            queue_item["media_image"] = ""
            if self._download_local:
                try:
                    LOGGER.debug("Expected to download locally.")
                    img_data = queue_item["media_item"]["metadata"]["images"][0]
                    url = generate_image_url_from_image_data(img_data, self._client)
                    LOGGER.debug(f"Downloading URL {url}")
                    result = await download_and_encode_image(url)
                    LOGGER.debug("Downloaded and setting")
                    queue_item["local_image_encoded"] = result
                except Exception as e:  # noqa: BLE001
                    LOGGER.debug(
                        f"Received error {e} when downloading image for queue item: {queue_item}",
                    )
            else:
                LOGGER.debug("No media image found but not expected to download.")
        return queue_item

    async def _process_queue_images(self, queue_items: list, queue_id: str):
        """Helper to process all images in a given queue."""
        items = [item if type(item) is dict else item.to_dict() for item in queue_items]
        try:
            result = await asyncio.gather(
                *[self.process_image_single_item(item) for item in items],
            )
        except:  # noqa: E722
            LOGGER.error(f"Unable to process queue items {items}!")
            result = items
        self.queues[queue_id] = result
        return result

    @callback
    def process_queue_images(self, queue_items: list, queue_id: str):
        """Processes all images in a given queue."""
        loop = self._hass.loop
        LOGGER.debug(f"Processing queue: {queue_items}")
        loop.create_task(self._process_queue_images(queue_items, queue_id))
