"""Class representing Sonos favorites."""
from __future__ import annotations

from collections.abc import Iterator
import logging
import re
from typing import TYPE_CHECKING, Any

from soco import SoCo
from soco.data_structures import DidlFavorite
from soco.events_base import Event as SonosEvent
from soco.exceptions import SoCoException

from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send

from .const import SONOS_CREATE_FAVORITES_SENSOR, SONOS_FAVORITES_UPDATED
from .helpers import soco_error
from .household_coordinator import SonosHouseholdCoordinator

if TYPE_CHECKING:
    from .speaker import SonosSpeaker

_LOGGER = logging.getLogger(__name__)


class SonosFavorites(SonosHouseholdCoordinator):
    """Coordinator class for Sonos favorites."""

    def __init__(self, *args: Any) -> None:
        """Initialize the data."""
        super().__init__(*args)
        self._favorites: list[DidlFavorite] = []
        self.last_polled_ids: dict[str, int] = {}

    def __iter__(self) -> Iterator:
        """Return an iterator for the known favorites."""
        favorites = self._favorites.copy()
        return iter(favorites)

    def setup(self, soco: SoCo) -> None:
        """Override to send a signal on base class setup completion."""
        super().setup(soco)
        dispatcher_send(self.hass, SONOS_CREATE_FAVORITES_SENSOR, self)

    @property
    def count(self) -> int:
        """Return the number of favorites."""
        return len(self._favorites)

    def lookup_by_item_id(self, item_id: str) -> DidlFavorite | None:
        """Return the favorite object with the provided item_id."""
        return next((fav for fav in self._favorites if fav.item_id == item_id), None)

    async def async_update_entities(
        self, soco: SoCo, update_id: int | None = None
    ) -> None:
        """Update the cache and update entities."""
        updated = await self.hass.async_add_executor_job(
            self.update_cache, soco, update_id
        )
        if not updated:
            return

        async_dispatcher_send(
            self.hass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}"
        )

    async def async_process_event(
        self, event: SonosEvent, speaker: SonosSpeaker
    ) -> None:
        """Process the event payload in an async lock and update entities."""
        event_id = event.variables["favorites_update_id"]
        container_ids = event.variables["container_update_i_ds"]
        if not (match := re.search(r"FV:2,(\d+)", container_ids)):
            return

        container_id = int(match.groups()[0])
        event_id = int(event_id.split(",")[-1])

        async with self.cache_update_lock:
            last_poll_id = self.last_polled_ids.get(speaker.uid)
            if (
                self.last_processed_event_id
                and event_id <= self.last_processed_event_id
            ):
                # Skip updates if this event_id has already been seen
                if not last_poll_id:
                    self.last_polled_ids[speaker.uid] = container_id
                return

            if last_poll_id and container_id <= last_poll_id:
                return

            speaker.event_stats.process(event)
            _LOGGER.debug(
                "New favorites event %s from %s (was %s)",
                event_id,
                speaker.soco,
                self.last_processed_event_id,
            )
            self.last_processed_event_id = event_id
            await self.async_update_entities(speaker.soco, container_id)

    @soco_error()
    def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
        """Update cache of known favorites and return if cache has changed."""
        new_favorites = soco.music_library.get_sonos_favorites()

        # Polled update_id values do not match event_id values
        # Each speaker can return a different polled update_id
        last_poll_id = self.last_polled_ids.get(soco.uid)
        if last_poll_id and new_favorites.update_id <= last_poll_id:
            # Skip updates already processed
            return False
        self.last_polled_ids[soco.uid] = new_favorites.update_id

        _LOGGER.debug(
            "Processing favorites update_id %s for %s (was: %s)",
            new_favorites.update_id,
            soco,
            last_poll_id,
        )

        self._favorites = []
        for fav in new_favorites:
            try:
                # exclude non-playable favorites with no linked resources
                if fav.reference.resources:
                    self._favorites.append(fav)
            except SoCoException as ex:
                # Skip unknown types
                _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)

        _LOGGER.debug(
            "Cached %s favorites for household %s using %s",
            len(self._favorites),
            self.household_id,
            soco,
        )
        return True