"""Support for Start.ca Bandwidth Monitor."""

import asyncio
from datetime import timedelta
from http import HTTPStatus
import logging
from xml.parsers.expat import ExpatError

from aiohttp import ClientSession
import voluptuous as vol
import xmltodict

from homeassistant.components.sensor import (
    PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
)
from homeassistant.const import (
    CONF_API_KEY,
    CONF_MONITORED_VARIABLES,
    CONF_NAME,
    PERCENTAGE,
    UnitOfInformation,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "Start.ca"
CONF_TOTAL_BANDWIDTH = "total_bandwidth"

MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
REQUEST_TIMEOUT = 5  # seconds

SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
    SensorEntityDescription(
        key="usage",
        name="Usage Ratio",
        native_unit_of_measurement=PERCENTAGE,
        icon="mdi:percent",
    ),
    SensorEntityDescription(
        key="usage_gb",
        name="Usage",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="limit",
        name="Data limit",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="used_download",
        name="Used Download",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="used_upload",
        name="Used Upload",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:upload",
    ),
    SensorEntityDescription(
        key="used_total",
        name="Used Total",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="grace_download",
        name="Grace Download",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="grace_upload",
        name="Grace Upload",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:upload",
    ),
    SensorEntityDescription(
        key="grace_total",
        name="Grace Total",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="total_download",
        name="Total Download",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="total_upload",
        name="Total Upload",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
    SensorEntityDescription(
        key="used_remaining",
        name="Remaining",
        native_unit_of_measurement=UnitOfInformation.GIGABYTES,
        device_class=SensorDeviceClass.DATA_SIZE,
        icon="mdi:download",
    ),
)

SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]

PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_MONITORED_VARIABLES): vol.All(
            cv.ensure_list, [vol.In(SENSOR_KEYS)]
        ),
        vol.Required(CONF_API_KEY): cv.string,
        vol.Required(CONF_TOTAL_BANDWIDTH): cv.positive_int,
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    }
)


async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:
    """Set up the sensor platform."""
    websession = async_get_clientsession(hass)
    apikey = config[CONF_API_KEY]
    bandwidthcap = config[CONF_TOTAL_BANDWIDTH]

    ts_data = StartcaData(websession, apikey, bandwidthcap)
    ret = await ts_data.async_update()
    if ret is False:
        _LOGGER.error("Invalid Start.ca API key: %s", apikey)
        return

    name = config[CONF_NAME]
    monitored_variables = config[CONF_MONITORED_VARIABLES]
    if bandwidthcap <= 0:
        monitored_variables = list(
            filter(
                lambda itm: itm not in {"limit", "usage", "used_remaining"},
                monitored_variables,
            )
        )
    entities = [
        StartcaSensor(ts_data, name, description)
        for description in SENSOR_TYPES
        if description.key in monitored_variables
    ]
    async_add_entities(entities, True)


class StartcaSensor(SensorEntity):
    """Representation of Start.ca Bandwidth sensor."""

    def __init__(
        self, startcadata: StartcaData, name: str, description: SensorEntityDescription
    ) -> None:
        """Initialize the sensor."""
        self.entity_description = description
        self.startcadata = startcadata

        self._attr_name = f"{name} {description.name}"

    async def async_update(self) -> None:
        """Get the latest data from Start.ca and update the state."""
        await self.startcadata.async_update()
        sensor_type = self.entity_description.key
        if sensor_type in self.startcadata.data:
            self._attr_native_value = round(self.startcadata.data[sensor_type], 2)


class StartcaData:
    """Get data from Start.ca API."""

    def __init__(
        self, websession: ClientSession, api_key: str, bandwidth_cap: int
    ) -> None:
        """Initialize the data object."""
        self.websession = websession
        self.api_key = api_key
        self.bandwidth_cap = bandwidth_cap
        # Set unlimited users to infinite, otherwise the cap.
        self.data = {}
        if self.bandwidth_cap > 0:
            self.data["limit"] = self.bandwidth_cap

    @staticmethod
    def bytes_to_gb(value):
        """Convert from bytes to GB.

        :param value: The value in bytes to convert to GB.
        :return: Converted GB value
        """
        return float(value) * 10**-9

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    async def async_update(self) -> bool:
        """Get the Start.ca bandwidth data from the web service."""
        _LOGGER.debug("Updating Start.ca usage data")
        url = f"https://www.start.ca/support/usage/api?key={self.api_key}"
        async with asyncio.timeout(REQUEST_TIMEOUT):
            req = await self.websession.get(url)
        if req.status != HTTPStatus.OK:
            _LOGGER.error("Request failed with status: %u", req.status)
            return False

        data = await req.text()
        try:
            xml_data = xmltodict.parse(data)
        except ExpatError:
            return False

        used_dl = self.bytes_to_gb(xml_data["usage"]["used"]["download"])
        used_ul = self.bytes_to_gb(xml_data["usage"]["used"]["upload"])
        grace_dl = self.bytes_to_gb(xml_data["usage"]["grace"]["download"])
        grace_ul = self.bytes_to_gb(xml_data["usage"]["grace"]["upload"])
        total_dl = self.bytes_to_gb(xml_data["usage"]["total"]["download"])
        total_ul = self.bytes_to_gb(xml_data["usage"]["total"]["upload"])

        if self.bandwidth_cap > 0:
            self.data["usage"] = 100 * used_dl / self.bandwidth_cap
            self.data["used_remaining"] = self.data["limit"] - used_dl
        self.data["usage_gb"] = used_dl
        self.data["used_download"] = used_dl
        self.data["used_upload"] = used_ul
        self.data["used_total"] = used_dl + used_ul
        self.data["grace_download"] = grace_dl
        self.data["grace_upload"] = grace_ul
        self.data["grace_total"] = grace_dl + grace_ul
        self.data["total_download"] = total_dl
        self.data["total_upload"] = total_ul

        return True
