"""Asynchronous Python client for AirGradient."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from importlib import metadata
import socket
from typing import TYPE_CHECKING, Any

from aiohttp import ClientError, ClientResponseError, ClientSession
from aiohttp.hdrs import METH_GET, METH_PUT
from mashumaro import MissingField
from yarl import URL

from .exceptions import AirGradientConnectionError, AirGradientParseError
from .models import (
    Config,
    ConfigurationControl,
    LedBarMode,
    Measures,
    PmStandard,
    TemperatureUnit,
    VersionCheck,
)

if TYPE_CHECKING:
    from typing_extensions import Self


VERSION = metadata.version(__package__)


@dataclass
class AirGradientClient:
    """Main class for handling connections with AirGradient."""

    host: str
    session: ClientSession | None = None
    request_timeout: int = 10
    _close_session: bool = False

    async def _request(
        self,
        url: URL,
        *,
        method: str = METH_GET,
        data: dict[str, Any] | None = None,
    ) -> str:
        """Handle a request to AirGradient."""
        headers = {
            "User-Agent": f"PythonAirGradient/{VERSION}",
            "Accept": "application/json",
        }

        if self.session is None:
            self.session = ClientSession()
            self._close_session = True

        try:
            async with asyncio.timeout(self.request_timeout):
                response = await self.session.request(
                    method,
                    url,
                    headers=headers,
                    json=data,
                )
        except asyncio.TimeoutError as exception:
            msg = "Timeout occurred while connecting to the device"
            raise AirGradientConnectionError(msg) from exception
        except (
            ClientError,
            ClientResponseError,
            socket.gaierror,
        ) as exception:
            msg = "Error occurred while communicating with the device"
            raise AirGradientConnectionError(msg) from exception

        if response.status != 200:
            content_type = response.headers.get("Content-Type", "")
            text = await response.text()
            msg = "Unexpected response from AirGradient"
            raise AirGradientConnectionError(
                msg,
                {"Content-Type": content_type, "response": text},
            )

        return await response.text()

    async def _request_device(
        self, uri: str, *, method: str = METH_GET, data: dict[str, Any] | None = None
    ) -> str:
        """Handle a request to the AirGradient device."""
        url = URL.build(scheme="http", host=self.host).joinpath(uri)
        return await self._request(url, method=method, data=data)

    async def get_current_measures(self) -> Measures:
        """Get current measures from AirGradient."""
        response = await self._request_device("measures/current")
        try:
            return Measures.from_json(response)
        except MissingField as err:
            raise AirGradientParseError from err

    async def get_config(self) -> Config:
        """Get config from AirGradient device."""
        response = await self._request_device("config")
        try:
            return Config.from_json(response)
        except MissingField as err:
            raise AirGradientParseError from err

    async def _set_config(self, field: str, value: Any) -> None:
        """Set config on AirGradient device."""
        await self._request_device("config", method=METH_PUT, data={field: value})

    async def set_pm_standard(self, pm_standard: PmStandard) -> None:
        """Set PM standard on AirGradient device."""
        await self._set_config("pmStandard", pm_standard)

    async def set_temperature_unit(self, temperature_unit: TemperatureUnit) -> None:
        """Set temperature unit on AirGradient device."""
        await self._set_config("temperatureUnit", temperature_unit)

    async def set_configuration_control(
        self, configuration_control: ConfigurationControl
    ) -> None:
        """Set configuration control on AirGradient device."""
        await self._set_config("configurationControl", configuration_control)

    async def set_led_bar_mode(self, led_bar_mode: LedBarMode) -> None:
        """Set LED bar mode on AirGradient device."""
        await self._set_config("ledBarMode", led_bar_mode)

    async def request_co2_calibration(self) -> None:
        """Request CO2 calibration on AirGradient device."""
        await self._set_config("co2CalibrationRequested", value=True)

    async def request_led_bar_test(self) -> None:
        """Request LED bar test on AirGradient device."""
        await self._set_config("ledBarTestRequested", value=True)

    async def set_display_brightness(self, brightness: int) -> None:
        """Set display brightness on AirGradient device."""
        await self._set_config("displayBrightness", brightness)

    async def set_led_bar_brightness(self, brightness: int) -> None:
        """Set LED bar brightness on AirGradient device."""
        await self._set_config("ledBarBrightness", brightness)

    async def enable_sharing_data(self, *, enable: bool) -> None:
        """Enable or disable sharing data on AirGradient device."""
        await self._set_config("postDataToAirGradient", value=enable)

    async def set_co2_automatic_baseline_calibration(self, days: int) -> None:
        """Set CO2 automatic baseline calibration on AirGradient device."""
        await self._set_config("abcDays", days)

    async def set_nox_learning_offset(self, offset: int) -> None:
        """Set NOx learning offset on AirGradient device."""
        await self._set_config("noxLearningOffset", offset)

    async def set_tvoc_learning_offset(self, offset: int) -> None:
        """Set TVOC learning offset on AirGradient device."""
        await self._set_config("tvocLearningOffset", offset)

    async def get_latest_firmware_version(self, serial_number: str) -> str:
        """Get the latest firmware version from AirGradient."""
        url = URL.build(scheme="http", host="hw.airgradient.com").joinpath(
            f"sensors/airgradient:{serial_number}/generic/os/firmware"
        )
        response = await self._request(url)
        try:
            return VersionCheck.from_json(response).target_version
        except MissingField as err:
            raise AirGradientParseError from err

    async def close(self) -> None:
        """Close open client session."""
        if self.session and self._close_session:
            await self.session.close()

    async def __aenter__(self) -> Self:
        """Async enter.

        Returns
        -------
            The AirGradientClient object.

        """
        return self

    async def __aexit__(self, *_exc_info: object) -> None:
        """Async exit.

        Args:
        ----
            _exc_info: Exec type.

        """
        await self.close()
