# ty:ignore[unresolved-import]
"""Initialize component."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from typing import TYPE_CHECKING

from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import (
    IssueSeverity,
    async_create_issue,
    async_delete_issue,
)
from music_assistant_client import MusicAssistantClient
from music_assistant_client.exceptions import (
    CannotConnect,
    InvalidServerVersion,
    MusicAssistantClientException,
)
from music_assistant_models.errors import (
    ActionUnavailable,
    AuthenticationFailed,
    AuthenticationRequired,
    InvalidToken,
    MusicAssistantError,
)

from .actions import (
    MassQueueActions,
    get_music_assistant_client,
    setup_controller_and_actions,
)
from .const import CONF_TOKEN, DOMAIN, LOGGER
from .services import register_actions
from .websocket_commands import (
    api_download_and_encode_image,
    api_download_images,
    api_get_entity_info,
    api_get_user_info,
)

if TYPE_CHECKING:
    from homeassistant.core import HomeAssistant
    from homeassistant.helpers.typing import ConfigType, Event

PLATFORMS = []

CONNECT_TIMEOUT = 10
LISTEN_READY_TIMEOUT = 30

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantQueueEntryData]


@dataclass
class MusicAssistantQueueEntryData:
    """Hold Mass data for the config entry."""

    mass: MusicAssistantClient
    actions: MassQueueActions
    listen_task: asyncio.Task


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:  # noqa: ARG001
    """Set up the Music Assistant component."""
    return True


async def async_setup_entry(  # noqa: PLR0915
    hass: HomeAssistant,
    entry: MusicAssistantConfigEntry,
) -> bool:
    """Set up Music Assistant from a config entry."""
    http_session = async_get_clientsession(hass, verify_ssl=False)
    mass_url = entry.data[CONF_URL]
    # Get token from config entry (for schema >= AUTH_SCHEMA_VERSION)
    token = entry.data.get(CONF_TOKEN)
    mass = MusicAssistantClient(mass_url, http_session, token=token)

    try:
        async with asyncio.timeout(CONNECT_TIMEOUT):
            await mass.connect()
    except (TimeoutError, CannotConnect) as err:
        exc = f"Failed to connect to music assistant server {mass_url}"
        raise ConfigEntryNotReady(
            exc,
        ) from err
    except InvalidServerVersion as err:
        async_create_issue(
            hass,
            DOMAIN,
            "invalid_server_version",
            is_fixable=False,
            severity=IssueSeverity.ERROR,
            translation_key="invalid_server_version",
        )
        exc = f"Invalid server version: {err}"
        raise ConfigEntryNotReady(exc) from err
    except (AuthenticationRequired, AuthenticationFailed, InvalidToken) as err:
        exc = f"Authentication failed for {mass_url}: {err}"
        raise ConfigEntryAuthFailed(exc) from err
    except MusicAssistantClientException as err:
        exc = f"Failed to connect to music assistant server {mass_url}: {err}"
        raise ConfigEntryNotReady(exc) from err
    except MusicAssistantError as err:
        LOGGER.exception("Failed to connect to music assistant server", exc_info=err)
        exc = f"Unknown error connecting to the Music Assistant server {mass_url}"
        raise ConfigEntryNotReady(
            exc,
        ) from err

    async_delete_issue(hass, DOMAIN, "invalid_server_version")

    async def on_hass_stop(event: Event) -> None:  # noqa: ARG001
        """Handle incoming stop event from Home Assistant."""
        await mass.disconnect()

    entry.async_on_unload(
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop),
    )

    # launch the music assistant client listen task in the background
    # use the init_ready event to wait until initialization is done
    init_ready = asyncio.Event()
    listen_task = asyncio.create_task(_client_listen(hass, entry, mass, init_ready))

    try:
        async with asyncio.timeout(LISTEN_READY_TIMEOUT):
            await init_ready.wait()
    except TimeoutError as err:
        listen_task.cancel()
        exc = "Music Assistant client not ready"
        raise ConfigEntryNotReady(exc) from err

    # store the listen task and mass client in the entry data
    actions = await setup_controller_and_actions(hass, mass, entry)
    register_actions(hass)
    entry.runtime_data = MusicAssistantQueueEntryData(mass, actions, listen_task)
    websocket_api.async_register_command(hass, api_download_images)
    websocket_api.async_register_command(hass, api_download_and_encode_image)
    websocket_api.async_register_command(hass, api_get_entity_info)
    websocket_api.async_register_command(hass, api_get_user_info)

    # If the listen task is already failed, we need to raise ConfigEntryNotReady
    if listen_task.done() and (listen_error := listen_task.exception()) is not None:
        await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
        try:
            await mass.disconnect()
        finally:
            raise ConfigEntryNotReady(listen_error) from listen_error

    # initialize platforms
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    return True


async def _client_listen(
    hass: HomeAssistant,
    entry: ConfigEntry,
    mass: MusicAssistantClient,
    init_ready: asyncio.Event,
) -> None:
    """Listen with the client."""
    try:
        await mass.start_listening(init_ready)
    except MusicAssistantError as err:
        if entry.state != ConfigEntryState.LOADED:
            raise
        LOGGER.error("Failed to listen: %s", err)
    except Exception as err:  # pylint: disable=broad-except
        # We need to guard against unknown exceptions to not crash this task.
        if entry.state != ConfigEntryState.LOADED:
            raise
        LOGGER.exception("Unexpected exception: %s", err)

    if not hass.is_stopping:
        LOGGER.debug("Disconnected from server. Reloading integration")
        hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

    if unload_ok:
        mass_entry_data: MusicAssistantQueueEntryData = entry.runtime_data
        mass_entry_data.listen_task.cancel()
        await mass_entry_data.mass.disconnect()

    return unload_ok


async def async_remove_config_entry_device(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    device_entry: dr.DeviceEntry,
) -> bool:
    """Remove a config entry from a device."""
    player_id = next(
        (
            identifier[1]
            for identifier in device_entry.identifiers
            if identifier[0] == DOMAIN
        ),
        None,
    )
    if player_id is None:
        # this should not be possible at all, but guard it anyways
        return False
    mass = get_music_assistant_client(hass, config_entry.entry_id)
    if mass.players.get(player_id) is None:
        # player is already removed on the server, this is an orphaned device
        return True
    # try to remove the player from the server
    try:
        await mass.config.remove_player_config(player_id)
    except ActionUnavailable:
        return False
    else:
        return True
