"""Define a base coordinator for Bluesound entities."""

from __future__ import annotations

import asyncio
from collections.abc import Callable, Coroutine
import contextlib
from dataclasses import dataclass, replace
from datetime import timedelta
import logging

from pyblu import Input, Player, Preset, Status, SyncStatus
from pyblu.errors import PlayerUnreachableError

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

NODE_OFFLINE_CHECK_TIMEOUT = timedelta(minutes=3)
PRESET_AND_INPUTS_INTERVAL = timedelta(minutes=15)


@dataclass
class BluesoundRuntimeData:
    """Bluesound data class."""

    player: Player
    sync_status: SyncStatus
    coordinator: BluesoundCoordinator


@dataclass
class BluesoundData:
    """Define a class to hold Bluesound data."""

    sync_status: SyncStatus
    status: Status
    presets: list[Preset]
    inputs: list[Input]


type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]


def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]:
    """Cancel a task."""

    async def _cancel_task() -> None:
        task.cancel()
        with contextlib.suppress(asyncio.CancelledError):
            await task

    return _cancel_task


class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]):
    """Define an object to hold Bluesound data."""

    config_entry: BluesoundConfigEntry

    def __init__(
        self,
        hass: HomeAssistant,
        config_entry: BluesoundConfigEntry,
        player: Player,
        sync_status: SyncStatus,
    ) -> None:
        """Initialize."""
        self.player = player
        self._inital_sync_status = sync_status

        super().__init__(
            hass,
            logger=_LOGGER,
            config_entry=config_entry,
            name=sync_status.name,
        )

    async def _async_setup(self) -> None:
        preset = await self.player.presets()
        inputs = await self.player.inputs()
        status = await self.player.status()

        self.async_set_updated_data(
            BluesoundData(
                sync_status=self._inital_sync_status,
                status=status,
                presets=preset,
                inputs=inputs,
            )
        )

        status_loop_task = self.hass.async_create_background_task(
            self._poll_status_loop(),
            name=f"bluesound.poll_status_loop_{self.data.sync_status.id}",
        )
        self.config_entry.async_on_unload(cancel_task(status_loop_task))

        sync_status_loop_task = self.hass.async_create_background_task(
            self._poll_sync_status_loop(),
            name=f"bluesound.poll_sync_status_loop_{self.data.sync_status.id}",
        )
        self.config_entry.async_on_unload(cancel_task(sync_status_loop_task))

        presets_and_inputs_loop_task = self.hass.async_create_background_task(
            self._poll_presets_and_inputs_loop(),
            name=f"bluesound.poll_presets_and_inputs_loop_{self.data.sync_status.id}",
        )
        self.config_entry.async_on_unload(cancel_task(presets_and_inputs_loop_task))

    async def _async_update_data(self) -> BluesoundData:
        return self.data

    async def _poll_presets_and_inputs_loop(self) -> None:
        while True:
            await asyncio.sleep(PRESET_AND_INPUTS_INTERVAL.total_seconds())
            try:
                preset = await self.player.presets()
                inputs = await self.player.inputs()
                self.async_set_updated_data(
                    replace(
                        self.data,
                        presets=preset,
                        inputs=inputs,
                    )
                )
            except PlayerUnreachableError as ex:
                self.async_set_update_error(ex)
            except asyncio.CancelledError:
                return
            except Exception as ex:  # noqa: BLE001 - this loop should never stop
                self.async_set_update_error(ex)

    async def _poll_status_loop(self) -> None:
        """Loop which polls the status of the player."""
        while True:
            try:
                status = await self.player.status(
                    etag=self.data.status.etag, poll_timeout=120, timeout=125
                )
                self.async_set_updated_data(
                    replace(
                        self.data,
                        status=status,
                    )
                )
            except PlayerUnreachableError as ex:
                self.async_set_update_error(ex)
                await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
            except asyncio.CancelledError:
                return
            except Exception as ex:  # noqa: BLE001 - this loop should never stop
                self.async_set_update_error(ex)
                await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())

    async def _poll_sync_status_loop(self) -> None:
        """Loop which polls the sync status of the player."""
        while True:
            try:
                sync_status = await self.player.sync_status(
                    etag=self.data.sync_status.etag, poll_timeout=120, timeout=125
                )
                self.async_set_updated_data(
                    replace(
                        self.data,
                        sync_status=sync_status,
                    )
                )
            except PlayerUnreachableError as ex:
                self.async_set_update_error(ex)
                await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
            except asyncio.CancelledError:
                raise
            except Exception as ex:  # noqa: BLE001 - this loop should never stop
                self.async_set_update_error(ex)
                await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
