"""Handler for Sonos websockets."""

import asyncio
import logging
import sys
from typing import Any, cast

import aiohttp
from aiohttp import WSMsgType

from .const import API_KEY, CLIP_ID_KEY, MAX_ATTEMPTS
from .exception import (
    SonosWebsocketError,
    SonosWSConnectionError,
    Unauthorized,
    Unsupported,
)

if sys.version_info[:2] < (3, 11):
    from async_timeout import timeout as asyncio_timeout
else:
    from asyncio import timeout as asyncio_timeout


_LOGGER = logging.getLogger(__name__)


class SonosWebsocket:
    """Sonos websocket handler."""

    def __init__(
        self,
        ip_addr: str,
        player_id: str | None = None,
        household_id: str | None = None,
        session: aiohttp.ClientSession | None = None,
        timeout: int = 6,
    ) -> None:
        """Initialize the websocket instance."""
        self.uri = f"wss://{ip_addr}:1443/websocket/api"
        self._own_session = not session
        self.session = session or aiohttp.ClientSession()
        self.ws: aiohttp.ClientWebSocketResponse | None = None
        self._household_id = household_id
        self._player_id = player_id
        self._connect_lock = asyncio.Lock()
        self._timeout = timeout

    async def connect(self) -> None:
        """Open a persistent websocket connection and act on events."""
        async with self._connect_lock:
            if self.ws and not self.ws.closed:
                _LOGGER.warning("Websocket is already connected")
                return

        _LOGGER.debug("Opening websocket to %s", self.uri)
        headers = {
            "X-Sonos-Api-Key": API_KEY,
            "Sec-WebSocket-Protocol": "v1.api.smartspeaker.audio",
        }
        try:
            async with asyncio_timeout(self._timeout):
                self.ws = await self.session.ws_connect(
                    self.uri, headers=headers, verify_ssl=False
                )
        except aiohttp.ClientResponseError as exc:
            if exc.code == 401:
                _LOGGER.error("Credentials rejected: %s", exc)
                raise Unauthorized("Credentials rejected") from exc
            raise SonosWSConnectionError(
                f"Unexpected response received: {exc}"
            ) from exc
        except aiohttp.ClientConnectionError as exc:
            raise SonosWSConnectionError(f"Connection error: {exc}") from exc
        except asyncio.TimeoutError as exc:
            raise SonosWSConnectionError("Connection timed out") from exc
        except Exception as exc:  # pylint: disable=broad-except
            raise SonosWSConnectionError(f"Unknown error: {exc}") from exc
        _LOGGER.debug("Successfully connected to %s", self.uri)

    async def close(self):
        """Close the websocket connection."""
        if self.ws and not self.ws.closed:
            await self.ws.close()
        if self._own_session and self.session and not self.session.closed:
            await self.session.close()

    async def send_command(
        self, command: dict[str, Any], options: dict[str, Any] | None = None
    ) -> list[dict[str, Any]]:
        """Send commands over the websocket and handle their responses."""
        attempt = 1
        while attempt <= MAX_ATTEMPTS:
            if not self.ws or self.ws.closed:
                await self.connect()
                assert self.ws

            payload = [command, options or {}]
            _LOGGER.debug("Sending command: %s", payload)
            try:
                async with asyncio_timeout(self._timeout):
                    await self.ws.send_json(payload)
                    msg = await self.ws.receive()
            except asyncio.TimeoutError:
                _LOGGER.error("Command timed out")
            except ConnectionResetError:
                # Websocket closing
                self.ws = None
                _LOGGER.debug("Websocket connection reset, will try again")
            else:
                if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING):
                    _LOGGER.debug("Websocket closed, will try again")
                elif msg.type != WSMsgType.TEXT:
                    _LOGGER.error("Received non-text message: %s", msg.type.name)
                else:
                    return msg.json()
            attempt += 1

        command_name = command.get("command", "Empty")
        raise SonosWebsocketError(
            f"{command_name} command failed after {MAX_ATTEMPTS} attempts"
        )

    async def play_clip(
        self, uri: str, volume: int | None = None
    ) -> list[dict[str, Any]]:
        """Play an audio clip."""
        command = {
            "namespace": "audioClip:1",
            "command": "loadAudioClip",
            "playerId": await self.get_player_id(),
        }
        options: dict[str, Any] = {
            "name": "Sonos Websocket",
            "appId": "com.jjlawren.sonos_websocket",
            "streamUrl": uri,
        }
        if volume:
            options["volume"] = volume
        return await self.send_command(command, options)

    async def cancel_clip(self, clip_id: str) -> list[dict[str, Any]]:
        """Cancel an active audio clip by its ID."""
        command = {
            "namespace": "audioClip:1",
            "command": "cancelAudioClip",
            "playerId": await self.get_player_id(),
        }
        options: dict[str, Any] = {CLIP_ID_KEY: clip_id}
        return await self.send_command(command, options)

    async def get_household_id(self) -> str:
        """Get the household ID of this device.

        Note: This is an invalid command but returns the household ID anyway.
        """
        if self._household_id:
            return self._household_id
        response, _ = await self.send_command({})
        if household_id := response.get("householdId"):
            self._household_id = household_id
            return household_id
        raise SonosWebsocketError("Could not determine household ID")

    async def get_groups(self) -> list[dict[str, Any]]:
        """Return the current group and player configuration."""
        command = {
            "namespace": "groups:1",
            "command": "getGroups",
            "householdId": await self.get_household_id(),
        }
        return await self.send_command(command)

    async def get_player_id(self) -> str:
        """Retrieve the player identifier for this speaker."""
        if self._player_id:
            return self._player_id
        response, data = await self.get_groups()
        if not response["success"]:
            raise SonosWebsocketError(f"Retrieving group data failed: {data}")
        if player := next(
            (p for p in data["players"] if p["websocketUrl"] == self.uri), None
        ):
            if "AUDIO_CLIP" not in player["capabilities"]:
                raise Unsupported("Device does not support AUDIO_CLIP")
            self._player_id = cast(str, player["id"])
            return self._player_id
        raise SonosWebsocketError("No matching player found in group data")
