"""Entity representing a Sonos battery level."""
from __future__ import annotations

import contextlib
import datetime
import logging
from typing import Any

from pysonos.core import SoCo
from pysonos.events_base import Event as SonosEvent
from pysonos.exceptions import SoCoException

from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect,
    async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util import dt as dt_util

from . import SonosData
from .const import (
    BATTERY_SCAN_INTERVAL,
    DATA_SONOS,
    SONOS_DISCOVERY_UPDATE,
    SONOS_ENTITY_CREATED,
    SONOS_PROPERTIES_UPDATE,
)
from .entity import SonosEntity
from .speaker import SonosSpeaker

_LOGGER = logging.getLogger(__name__)

ATTR_BATTERY_LEVEL = "battery_level"
ATTR_BATTERY_CHARGING = "charging"
ATTR_BATTERY_POWERSOURCE = "power_source"

EVENT_CHARGING = {
    "CHARGING": True,
    "NOT_CHARGING": False,
}


def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None:
    """Fetch battery_info from the given SoCo object.

    Returns None if the device doesn't support battery info
    or if the device is offline.
    """
    with contextlib.suppress(ConnectionError, TimeoutError, SoCoException):
        return soco.get_battery_info()


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up Sonos from a config entry."""

    sonos_data = hass.data[DATA_SONOS]

    async def _async_create_entity(speaker: SonosSpeaker) -> SonosBatteryEntity | None:
        if battery_info := await hass.async_add_executor_job(
            fetch_battery_info_or_none, speaker.soco
        ):
            return SonosBatteryEntity(speaker, sonos_data, battery_info)
        return None

    async def _async_create_entities(speaker: SonosSpeaker):
        if entity := await _async_create_entity(speaker):
            async_add_entities([entity])
        else:
            async_dispatcher_send(
                hass, f"{SONOS_ENTITY_CREATED}-{speaker.soco.uid}", SENSOR_DOMAIN
            )

    async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, _async_create_entities)


class SonosBatteryEntity(SonosEntity, Entity):
    """Representation of a Sonos Battery entity."""

    def __init__(
        self, speaker: SonosSpeaker, sonos_data: SonosData, battery_info: dict[str, Any]
    ):
        """Initialize a SonosBatteryEntity."""
        super().__init__(speaker, sonos_data)
        self._battery_info: dict[str, Any] = battery_info
        self._last_event: datetime.datetime = None

    async def async_added_to_hass(self) -> None:
        """Register polling callback when added to hass."""
        await super().async_added_to_hass()

        self.async_on_remove(
            self.hass.helpers.event.async_track_time_interval(
                self.async_update, BATTERY_SCAN_INTERVAL
            )
        )
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}",
                self.async_update_battery_info,
            )
        )
        async_dispatcher_send(
            self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", SENSOR_DOMAIN
        )

    async def async_update_battery_info(self, event: SonosEvent = None) -> None:
        """Update battery info using the provided SonosEvent."""
        if event is None:
            return

        if (more_info := event.variables.get("more_info")) is None:
            return

        more_info_dict = dict(x.split(":") for x in more_info.split(","))
        self._last_event = dt_util.utcnow()

        is_charging = EVENT_CHARGING[more_info_dict["BattChg"]]
        if is_charging == self.charging:
            self._battery_info.update({"Level": int(more_info_dict["BattPct"])})
        else:
            if battery_info := await self.hass.async_add_executor_job(
                fetch_battery_info_or_none, self.soco
            ):
                self._battery_info = battery_info

        self.async_write_ha_state()

    @property
    def unique_id(self) -> str:
        """Return the unique ID of the sensor."""
        return f"{self.soco.uid}-battery"

    @property
    def name(self) -> str:
        """Return the name of the sensor."""
        return f"{self.speaker.zone_name} Battery"

    @property
    def device_class(self) -> str:
        """Return the entity's device class."""
        return DEVICE_CLASS_BATTERY

    @property
    def unit_of_measurement(self) -> str:
        """Get the unit of measurement."""
        return PERCENTAGE

    async def async_update(self, event=None) -> None:
        """Poll the device for the current state."""
        if not self.available:
            # wait for the Sonos device to come back online
            return

        if (
            self._last_event
            and dt_util.utcnow() - self._last_event < BATTERY_SCAN_INTERVAL
        ):
            return

        if battery_info := await self.hass.async_add_executor_job(
            fetch_battery_info_or_none, self.soco
        ):
            self._battery_info = battery_info
            self.async_write_ha_state()

    @property
    def battery_level(self) -> int:
        """Return the battery level."""
        return self._battery_info.get("Level", 0)

    @property
    def power_source(self) -> str:
        """Return the name of the power source.

        Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER.
        """
        return self._battery_info.get("PowerSource", STATE_UNKNOWN)

    @property
    def charging(self) -> bool:
        """Return the charging status of this battery."""
        return self.power_source not in ("BATTERY", STATE_UNKNOWN)

    @property
    def icon(self) -> str:
        """Return the icon of the sensor."""
        return icon_for_battery_level(self.battery_level, self.charging)

    @property
    def state(self) -> int | None:
        """Return the state of the sensor."""
        return self._battery_info.get("Level")

    @property
    def device_state_attributes(self) -> dict[str, Any]:
        """Return entity specific state attributes."""
        return {
            ATTR_BATTERY_CHARGING: self.charging,
            ATTR_BATTERY_POWERSOURCE: self.power_source,
        }