"""Python implementation of the mclimate api."""

import json
import logging
from datetime import UTC, datetime
from importlib.metadata import PackageNotFoundError, version
from numbers import Number
from typing import Any, Dict, Optional, Union

from aiohttp import ClientSession

from .const import (
    CHANGE_THRESHOLD,
    CHANGE_TIME_CACHE_DEFAULT,
    CLIENT_DATA,
    HEADERS,
    MELISSA_URL,
    MIN_HUMIDITY_ALLOWED,
)
from .exceptions import ApiException, UnsupportedDevice

try:
    __version__ = version("melissa")
except PackageNotFoundError:
    __version__ = "dev"

__author__ = "Magnus Knutas"

_LOGGER = logging.getLogger(__name__)

__all__ = ("ApiException", "AsyncMelissa", "MELISSA_URL")


class AsyncMelissa:
    """Async class for Melissa."""

    SERIAL_NUMBER: str = "serial_number"
    COMMAND: str = "command"
    STATE: str = "state"
    MODE: str = "mode"
    TEMP: str = "temp"
    FAN: str = "fan"
    HUMIDITY: str = "humidity"

    FAN_AUTO: int = 0
    FAN_LOW: int = 1
    FAN_MEDIUM: int = 2
    FAN_HIGH: int = 3

    STATE_OFF: int = 0
    STATE_ON: int = 1
    STATE_IDLE: int = 2

    MODE_AUTO: int = 0
    MODE_FAN: int = 1
    MODE_HEAT: int = 2
    MODE_COOL: int = 3
    MODE_DRY: int = 4

    LED_ON: str = "reset_color"
    LED_OFF: str = "turn_led_off"

    DEFAULT_DATA_MELISSA: Dict[str, Union[int, str]] = {
        SERIAL_NUMBER: "",
        COMMAND: "send_ir_code",
        STATE: STATE_ON,  # 0-OFF; 1-ON; 2-Idle
        MODE: MODE_HEAT,  # 0-Auto; 1-Fan; 2-Heat; 3-Cool; 4-Dry
        TEMP: 20,  # number between 16 and 30 (depends on the codeset)
        FAN: FAN_MEDIUM,  # 0-Auto; 1-Low; 2-Med; 3-High
    }

    DEFAULT_DATA_BOBBIE: Dict[str, str] = {
        SERIAL_NUMBER: "",
        COMMAND: "switch_on_off",
        STATE: "on",
    }

    DEFAULT_DATA_LED: Dict[str, str] = {SERIAL_NUMBER: "", COMMAND: LED_ON}

    def __init__(
        self,
        username: str,
        password: str,
        headers: Dict[str, str] = HEADERS,
        time_cache: int = CHANGE_TIME_CACHE_DEFAULT,
        access_token: Optional[str] = None,
        token_type: Optional[str] = None,
        refresh_token: Optional[str] = None,
    ) -> None:
        """Init method."""
        self.default_headers: Dict[str, str] = headers
        self.access_token: Optional[str] = access_token
        self.token_type: Optional[str] = token_type
        self._latest_status: Dict[str, Any] = {}
        self.session: ClientSession = ClientSession(connector_owner=False)
        self.username: str = username
        self.password: str = password
        self.refresh_token: Optional[str] = refresh_token
        self.devices: Dict[str, Dict[str, Any]] = {}
        self.geofences: Dict[str, Dict[str, Any]] = {}
        self._latest_humidity: Optional[float] = None
        self._latest_temp: Optional[float] = None
        self._time_cache: int = time_cache
        self.fetch_timestamp: Optional[datetime] = None

    def _get_headers(self) -> Dict[str, str]:
        headers = self.default_headers.copy()
        headers.update(
            {"Authorization": "%s %s" % (self.token_type, self.access_token)}
        )
        _LOGGER.debug(headers)
        return headers

    def sanity_check(self, data: Dict[str, Any], device: str) -> bool:
        """Sanity check method."""
        ret = True
        _latest: Optional[Dict[str, Union[str, Number, None]]] = (
            self._latest_status.get(device)
        )
        if _latest is not None:
            _LOGGER.debug(data)
            left = data[self.TEMP]
            right = _latest[self.TEMP]
            if abs(left - right) > CHANGE_THRESHOLD:
                ret = False
        if (
            self._latest_status
            and self._latest_status.get(device)
            and data[self.HUMIDITY] < MIN_HUMIDITY_ALLOWED
        ):
            ret = False
        return ret

    @property
    def have_connection(self) -> bool:
        """Have connection method."""
        return self.access_token is not None

    async def async_connect(self) -> None:
        """Async connect method."""
        url = MELISSA_URL % "auth/login"
        _LOGGER.info(url)
        data = CLIENT_DATA.copy()
        data.update({"username": self.username, "password": self.password})
        _LOGGER.info(data)
        req = await self.session.post(url, data=data, headers=HEADERS)
        if req.status == 200:
            resp = json.loads(await req.text())
            self.access_token = resp["auth"]["access_token"]
            self.refresh_token = resp["auth"]["refresh_token"]
            self.token_type = resp["auth"]["token_type"]
        else:
            raise ApiException(await req.text(), req.status)

    async def async_fetch(self, url: str) -> str:
        """Async fetch method."""
        response = await self.session.get(url)
        ret: str = await response.text()
        return ret

    async def async_fetch_devices(self) -> Dict[str, Dict[str, Any]]:
        """Async fetch devices method."""
        url = MELISSA_URL % "controllers"
        _LOGGER.info(url)
        headers = self._get_headers()
        req = await self.session.get(url, headers=headers)
        if req.status == 200:
            resp = json.loads(await req.text())
            for controller in resp["_embedded"]["controller"]:
                self.devices[controller["serial_number"]] = controller
        _LOGGER.debug(self.devices)
        return self.devices

    async def async_fetch_geofences(self) -> Dict[str, Dict[str, Any]]:
        """Async fetch geofences method."""
        url = MELISSA_URL % "geofences"
        _LOGGER.info(url)
        headers = self._get_headers()
        req = await self.session.get(url, headers=headers)
        if req.status == 200:
            resp = json.loads(await req.text())
            for geofence in resp["_embedded"]["geofence"]:
                self.geofences[geofence["controller_id"]] = geofence
        _LOGGER.info(self.geofences)
        return self.geofences

    async def async_send(
        self,
        device: str,
        device_type: str = "melissa",
        state_data: Optional[Dict[str, Any]] = None,
    ) -> bool:
        """Async send method."""
        data: Dict[str, Any]
        if device_type == "melissa":
            data = self.DEFAULT_DATA_MELISSA.copy()
        elif device_type == "bobbie":
            data = self.DEFAULT_DATA_BOBBIE.copy()
        elif device_type == "led":
            data = self.DEFAULT_DATA_LED.copy()
        else:
            raise UnsupportedDevice(device_type)

        if state_data:
            data.update(state_data)

        data.update({self.SERIAL_NUMBER: device})
        url = MELISSA_URL % "provider/send"
        _LOGGER.info(url)

        if not self.have_connection:
            await self.async_connect()

        headers = self._get_headers()
        headers.update({"Content-Type": "application/json"})

        input_data = json.dumps(data)
        _LOGGER.debug(input_data)
        req = await self.session.post(url, data=input_data, headers=headers)
        if not req.status == 200:
            raise ApiException(await req.text(), req.status)

        return True

    async def async_status(
        self, test: bool = False, cached: bool = False
    ) -> Dict[str, Any]:
        """Async status method."""
        if (
            cached
            and self.fetch_timestamp
            and self._time_cache
            > (datetime.now(UTC) - self.fetch_timestamp).total_seconds()
        ):
            return self._latest_status
        url = MELISSA_URL % "provider/fetch"
        _LOGGER.info(url)
        headers = self._get_headers()
        headers.update({"Content-Type": "application/json"})
        ret = {}
        if not self.devices:
            await self.async_fetch_devices()
        for device in self.devices:
            if str(self.devices[device]["type"]) in ("melissa", "bobbie"):
                input_data = json.dumps({"serial_number": device})
                req = await self.session.post(
                    url, data=input_data, headers=headers
                )
                if req.status == 200:
                    data = json.loads(await req.text())
                    ret[device] = data["provider"]
                elif req.status == 401 and not test:
                    await self.async_connect()
                    return await self.async_status()
                else:
                    raise ApiException(await req.text())
        self.fetch_timestamp = datetime.now(UTC)
        self._latest_status = ret
        return ret

    async def async_cur_settings(self, serial_number: str) -> Dict[str, Any]:
        """Async cur settings method."""
        url = MELISSA_URL % "controllers/%s" % serial_number
        _LOGGER.info(url)
        headers = self._get_headers()
        req = await self.session.get(url, headers=headers)
        if req.status == 200:
            data: Dict[str, Any] = json.loads(await req.text())
        else:
            raise ApiException(await req.text(), req.status)
        _LOGGER.debug(data)
        return data
