"""Helpers for Renault API."""

import asyncio
import functools
import logging

import aiohttp

from .const import AVAILABLE_LOCALES
from .const import CONF_GIGYA_APIKEY
from .const import CONF_GIGYA_URL
from .const import CONF_KAMEREON_APIKEY
from .const import CONF_KAMEREON_URL
from .const import LOCALE_BASE_URL
from .const import MAX_SOC_MIN
from .const import MAX_SOC_TARGET
from .const import MIN_SOC_MIN
from .const import MIN_SOC_TARGET
from .const import SOC_STEP
from .exceptions import InvalidInputError
from .exceptions import RenaultException

_LOGGER = logging.getLogger(__package__)


async def get_api_keys(
    locale: str,
    force_load: bool = False,
    websession: aiohttp.ClientSession | None = None,
) -> dict[str, str]:
    """Get the API keys for specified locale.

    Args:
        locale (str): locale code (preferrably from AVAILABLE_LOCALES.keys())
        force_load (bool): bypass internal AVAILABLE_LOCALES
        websession (aiohttp.ClientSession): required if locale not in AVAILABLE_LOCALES

    Returns:
        Dict with gigya-api-key, gigya-api-url,
        kamereon-api-key and kamereon-api-url

    Raises:
        RenaultException: an issue occured loading the API keys
    """
    if locale in AVAILABLE_LOCALES.keys() and not force_load:
        return AVAILABLE_LOCALES[locale]
    else:
        _LOGGER.warning(
            "Locale %s was not found in AVAILABLE_LOCALES "
            "(or force_load used). "
            "Attempting to load details from Renault servers.",
            locale,
        )
        if websession is None:
            raise RenaultException("aiohttp_session is not set.")

        url = f"{LOCALE_BASE_URL}/configuration/android/config_{locale}.json"
        async with websession.get(url) as response:
            try:
                response.raise_for_status()
            except aiohttp.ClientResponseError as exc:
                raise RenaultException(
                    f"Locale not found on Renault server (HTTPStatus = {exc.status})."
                ) from exc
            # Server sometimes returns invalid content-type
            # eg. application/octet-stream
            response_body = await response.json(content_type=None)

            _LOGGER.debug(
                "Received api keys from myrenault response: %s", response_body
            )

            servers = response_body["servers"]
            return {
                CONF_GIGYA_APIKEY: servers["gigyaProd"]["apikey"],
                CONF_GIGYA_URL: servers["gigyaProd"]["target"],
                CONF_KAMEREON_APIKEY: servers["wiredProd"]["apikey"],
                CONF_KAMEREON_URL: servers["wiredProd"]["target"],
            }


def create_aiohttp_closed_event(
    websession: aiohttp.ClientSession,
) -> asyncio.Event:
    """Work around aiohttp issue that doesn't properly close transports on exit.

    See https://github.com/aio-libs/aiohttp/issues/1925#issuecomment-639080209

    Args:
        websession (aiohttp.ClientSession): session for which to generate the event.

    Returns:
        An event that will be set once all transports have been properly closed.
    """
    transports = 0
    all_is_lost = asyncio.Event()

    def connection_lost(exc, orig_lost):  # type: ignore
        nonlocal transports

        try:
            orig_lost(exc)
        finally:
            transports -= 1
            if transports == 0:
                all_is_lost.set()

    def eof_received(orig_eof_received):  # type: ignore
        try:
            orig_eof_received()
        except AttributeError:
            # It may happen that eof_received() is called after
            # _app_protocol and _transport are set to None.
            pass

    for conn in websession.connector._conns.values():  # type: ignore
        for handler, _ in conn:
            proto = getattr(handler.transport, "_ssl_protocol", None)
            if proto is None:
                continue

            transports += 1
            orig_lost = proto.connection_lost
            orig_eof_received = proto.eof_received

            proto.connection_lost = functools.partial(
                connection_lost, orig_lost=orig_lost
            )
            proto.eof_received = functools.partial(
                eof_received, orig_eof_received=orig_eof_received
            )

    if transports == 0:
        all_is_lost.set()

    return all_is_lost


def validate_battery_soc_input(*, min: int, target: int) -> None:
    if min < MIN_SOC_MIN or min > MAX_SOC_MIN or min % SOC_STEP != 0:
        raise InvalidInputError(
            f"Minimum state of charge level must be between {MIN_SOC_MIN} and "
            f"{MAX_SOC_MIN} with a step of {SOC_STEP}."
        )
    if target < MIN_SOC_TARGET or target > MAX_SOC_TARGET or target % SOC_STEP != 0:
        raise InvalidInputError(
            f"Target state of charge level must be between {MIN_SOC_TARGET} and "
            f"{MAX_SOC_TARGET} with a step of {SOC_STEP}."
        )
