import asyncio
from datetime import datetime
from importlib import metadata
from typing import Any

from aiohttp import ClientSession

from .probe import Probe

VERSION = metadata.version(__package__)


class InvalidAuth(Exception):
    def __init__(self, message: str):
        self.message = message
        super().__init__(self.message)


class Timeout(Exception):
    pass


class CustomerApi:

    API_TIMEOUT_NORMAL = 3
    API_TIMEOUT_SLOW = 10
    DEFAULT_LANGUAGE = "en"

    api_base_url = "https://monitor.sensoterra.com/api/v3"

    def __init__(self, email: str | None = None, password: str | None = None):
        self.__email = email
        self.__password = password
        self.__headers = {
            "User-Agent": "Sensoterra Python API/{}".format(self.get_version()),
            "language": self.DEFAULT_LANGUAGE,
        }
        self.__probes: dict[str, Probe] = {}
        self.__api_token: str | None = None
        self.__include_shared_data = True

    async def __auth(self, session: ClientSession) -> None:
        """Authenticate using either email and password, or an API token"""
        if self.__api_token is None:
            async with session.post(
                f"{self.api_base_url}/customer/auth",
                json={"email": self.__email, "password": self.__password},
                timeout=self.API_TIMEOUT_NORMAL,
            ) as resp:
                if resp.status >= 400 and resp.status <= 499:
                    json = await resp.json()
                    raise InvalidAuth(json["message"])
                resp.raise_for_status()
                json = await resp.json()
                api_key = json["api_key"]
        else:
            api_key = self.__api_token

        self.__headers["api_key"] = api_key

    async def __request(
        self,
        session: ClientSession,
        method: str,
        command: str,
        params: dict[str, str] | None = None,
        json: dict[str, str] | None = None,
    ) -> Any:
        """Wrapper to requests.request

        Returns:
        None: in case a http status 429 is received, or when poll too often
        Response: request response object
        """

        for i in range(2):
            if "api_key" not in self.__headers:
                await self.__auth(session)
            async with session.request(
                method,
                f"{self.api_base_url}/{command}",
                headers=self.__headers,
                params=params,
                json=json,
                timeout=self.API_TIMEOUT_SLOW,
            ) as resp:
                if resp.status == 403:  # Forbidden
                    await self.__auth(session)
                    json = None
                elif resp.status == 401:  # Unauthorized
                    json = await resp.json()
                    msg = json["message"] if json is not None else "Unauthorized"
                    raise InvalidAuth(msg)
                else:
                    resp.raise_for_status()
                    json = await resp.json()
                    break

        return json

    async def __get_locations(self, session: ClientSession) -> dict[int, str]:
        """Get all locations"""
        params = {"include_shared_data": "YES" if self.__include_shared_data else "NO"}

        locations = await self.__request(session, "GET", "location", params=params)

        return {location["id"]: location["name"] for location in locations}

    async def __get_soils(self, session: ClientSession) -> dict[str, str]:
        """Get all soil types"""

        parameters = await self.__request(session, "GET", "parameter")

        return {
            parameter["key"]: parameter["name"]
            for parameter in parameters
            if parameter["type"] == "SOIL"
        }

    async def __get_probes(self, session: ClientSession) -> dict[str, str]:
        """Get all probes"""
        params = {"include_shared_data": "YES" if self.__include_shared_data else "NO"}

        probes: dict[str, str] = await self.__request(
            session, "GET", "probe", params=params
        )
        return probes

    def get_version(self) -> str:
        return VERSION

    def set_language(self, language: str) -> None:
        """Set langage like for soil names

        Examples: es, en, en-us, nl
        """
        self.__headers["language"] = language

    def set_token(self, token: str) -> None:
        self.__api_token = token

    def get_include_shared_data(self) -> bool:
        return self.__include_shared_data

    def set_include_shared_data(self, include_shared_data: bool) -> None:
        self.__include_shared_data = include_shared_data

    async def get_token(self, tag: str, scope: str, expiration: datetime) -> str:
        async with ClientSession() as session:
            data = {
                "tag": tag,
                "scope": scope,
                "expiration": expiration.astimezone().isoformat(),
            }
            json = await self.__request(session, "POST", "token", json=data)
        return str(json["token"])

    async def poll(self) -> list[Probe]:
        """Update the list of probes"""
        try:
            async with ClientSession() as session:
                await self.__auth(session)
                async with asyncio.TaskGroup() as tg:
                    task_1 = tg.create_task(self.__get_probes(session))
                    task_2 = tg.create_task(self.__get_soils(session))
                    task_3 = tg.create_task(self.__get_locations(session))
                probes: list[dict[str, Any]] = task_1.result()  # type: ignore
                soils = task_2.result()
                locations = task_3.result()
        except ExceptionGroup as exp:
            for inner in exp.exceptions:
                if isinstance(inner, InvalidAuth):
                    raise inner
                if isinstance(inner, TimeoutError):
                    raise Timeout
            raise exp

        for probe in probes:
            if probe["serial"] not in self.__probes:
                self.__probes[probe["serial"]] = Probe(probe["serial"])
            self.__probes[probe["serial"]].update(soils, locations, probe)

        return list(self.__probes.values())
